Skip to main content

squawk_line_index/
newlines.rs

1// via: https://github.com/astral-sh/ruff/blob/2ddadb9937e0fe579bcd0904cec50d067f176edd/crates/ruff_source_file/src/newlines.rs
2//
3// MIT License
4//
5// Copyright (c) 2022 Charles Marsh
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in all
15// copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23// SOFTWARE.
24use std::iter::FusedIterator;
25use std::ops::Deref;
26
27use memchr::{memchr2, memrchr2};
28use text_size::{TextLen, TextRange, TextSize};
29
30/// Extension trait for [`str`] that provides a [`UniversalNewlineIterator`].
31pub trait UniversalNewlines {
32    fn universal_newlines(&self) -> UniversalNewlineIterator<'_>;
33}
34
35impl UniversalNewlines for str {
36    fn universal_newlines(&self) -> UniversalNewlineIterator<'_> {
37        UniversalNewlineIterator::from(self)
38    }
39}
40
41/// Like [`str::lines`], but accommodates LF, CRLF, and CR line endings,
42/// the latter of which are not supported by [`str::lines`].
43#[derive(Clone)]
44pub struct UniversalNewlineIterator<'a> {
45    text: &'a str,
46    offset: TextSize,
47    offset_back: TextSize,
48}
49
50impl<'a> UniversalNewlineIterator<'a> {
51    pub fn with_offset(text: &'a str, offset: TextSize) -> UniversalNewlineIterator<'a> {
52        UniversalNewlineIterator {
53            text,
54            offset,
55            offset_back: offset + text.text_len(),
56        }
57    }
58
59    pub fn from(text: &'a str) -> UniversalNewlineIterator<'a> {
60        Self::with_offset(text, TextSize::default())
61    }
62}
63
64/// Finds the next newline character. Returns its position and the [`LineEnding`].
65#[inline]
66pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> {
67    let bytes = text.as_bytes();
68    if let Some(position) = memchr2(b'\n', b'\r', bytes) {
69        let line_ending = match bytes[position] {
70            // Explicit branch for `\n` as this is the most likely path
71            b'\n' => LineEnding::Lf,
72            // '\r\n'
73            b'\r' if bytes.get(position.saturating_add(1)) == Some(&b'\n') => LineEnding::CrLf,
74            // '\r'
75            _ => LineEnding::Cr,
76        };
77
78        Some((position, line_ending))
79    } else {
80        None
81    }
82}
83
84impl<'a> Iterator for UniversalNewlineIterator<'a> {
85    type Item = Line<'a>;
86
87    #[inline]
88    fn next(&mut self) -> Option<Line<'a>> {
89        if self.text.is_empty() {
90            return None;
91        }
92
93        let line = if let Some((newline_position, line_ending)) = find_newline(self.text) {
94            let (text, remainder) = self.text.split_at(newline_position + line_ending.len());
95
96            let line = Line {
97                offset: self.offset,
98                text,
99            };
100
101            self.text = remainder;
102            self.offset += text.text_len();
103
104            line
105        }
106        // Last line
107        else {
108            Line {
109                offset: self.offset,
110                text: std::mem::take(&mut self.text),
111            }
112        };
113
114        Some(line)
115    }
116
117    fn last(mut self) -> Option<Self::Item> {
118        self.next_back()
119    }
120}
121
122impl DoubleEndedIterator for UniversalNewlineIterator<'_> {
123    #[inline]
124    fn next_back(&mut self) -> Option<Self::Item> {
125        if self.text.is_empty() {
126            return None;
127        }
128
129        let len = self.text.len();
130
131        // Trim any trailing newlines.
132        let haystack = match self.text.as_bytes()[len - 1] {
133            b'\n' if len > 1 && self.text.as_bytes()[len - 2] == b'\r' => &self.text[..len - 2],
134            b'\n' | b'\r' => &self.text[..len - 1],
135            _ => self.text,
136        };
137
138        // Find the end of the previous line. The previous line is the text up to, but not including
139        // the newline character.
140        let line = if let Some(line_end) = memrchr2(b'\n', b'\r', haystack.as_bytes()) {
141            // '\n' or '\r' or '\r\n'
142            let (remainder, line) = self.text.split_at(line_end + 1);
143            self.text = remainder;
144            self.offset_back -= line.text_len();
145
146            Line {
147                text: line,
148                offset: self.offset_back,
149            }
150        } else {
151            // Last line
152            let offset = self.offset_back - self.text.text_len();
153            Line {
154                text: std::mem::take(&mut self.text),
155                offset,
156            }
157        };
158
159        Some(line)
160    }
161}
162
163impl FusedIterator for UniversalNewlineIterator<'_> {}
164
165/// Like [`UniversalNewlineIterator`], but includes a trailing newline as an empty line.
166pub struct NewlineWithTrailingNewline<'a> {
167    trailing: Option<Line<'a>>,
168    underlying: UniversalNewlineIterator<'a>,
169}
170
171impl<'a> NewlineWithTrailingNewline<'a> {
172    pub fn from(input: &'a str) -> NewlineWithTrailingNewline<'a> {
173        Self::with_offset(input, TextSize::default())
174    }
175
176    pub fn with_offset(input: &'a str, offset: TextSize) -> Self {
177        NewlineWithTrailingNewline {
178            underlying: UniversalNewlineIterator::with_offset(input, offset),
179            trailing: if input.ends_with(['\r', '\n']) {
180                Some(Line {
181                    text: "",
182                    offset: offset + input.text_len(),
183                })
184            } else {
185                None
186            },
187        }
188    }
189}
190
191impl<'a> Iterator for NewlineWithTrailingNewline<'a> {
192    type Item = Line<'a>;
193
194    #[inline]
195    fn next(&mut self) -> Option<Self::Item> {
196        self.underlying.next().or_else(|| self.trailing.take())
197    }
198}
199
200impl DoubleEndedIterator for NewlineWithTrailingNewline<'_> {
201    #[inline]
202    fn next_back(&mut self) -> Option<Self::Item> {
203        self.trailing.take().or_else(|| self.underlying.next_back())
204    }
205}
206
207#[derive(Debug, Clone, Eq, PartialEq)]
208pub struct Line<'a> {
209    text: &'a str,
210    offset: TextSize,
211}
212
213impl<'a> Line<'a> {
214    pub fn new(text: &'a str, offset: TextSize) -> Self {
215        Self { text, offset }
216    }
217
218    #[inline]
219    pub const fn start(&self) -> TextSize {
220        self.offset
221    }
222
223    /// Returns the byte offset where the line ends, including its terminating new line character.
224    #[inline]
225    pub fn full_end(&self) -> TextSize {
226        self.offset + self.full_text_len()
227    }
228
229    /// Returns the byte offset where the line ends, excluding its new line character
230    #[inline]
231    pub fn end(&self) -> TextSize {
232        self.offset + self.as_str().text_len()
233    }
234
235    /// Returns the range of the line, including its terminating new line character.
236    #[inline]
237    pub fn full_range(&self) -> TextRange {
238        TextRange::at(self.offset, self.text.text_len())
239    }
240
241    /// Returns the range of the line, excluding its terminating new line character
242    #[inline]
243    pub fn range(&self) -> TextRange {
244        TextRange::new(self.start(), self.end())
245    }
246
247    /// Returns the line's new line character, if any.
248    #[inline]
249    pub fn line_ending(&self) -> Option<LineEnding> {
250        let mut bytes = self.text.bytes().rev();
251        match bytes.next() {
252            Some(b'\n') => {
253                if bytes.next() == Some(b'\r') {
254                    Some(LineEnding::CrLf)
255                } else {
256                    Some(LineEnding::Lf)
257                }
258            }
259            Some(b'\r') => Some(LineEnding::Cr),
260            _ => None,
261        }
262    }
263
264    /// Returns the text of the line, excluding the terminating new line character.
265    #[inline]
266    pub fn as_str(&self) -> &'a str {
267        let newline_len = self
268            .line_ending()
269            .map_or(0, |line_ending| line_ending.len());
270        &self.text[..self.text.len() - newline_len]
271    }
272
273    /// Returns the line's text, including the terminating new line character.
274    #[inline]
275    pub fn as_full_str(&self) -> &'a str {
276        self.text
277    }
278
279    #[inline]
280    fn full_text_len(&self) -> TextSize {
281        self.text.text_len()
282    }
283}
284
285impl Deref for Line<'_> {
286    type Target = str;
287
288    fn deref(&self) -> &Self::Target {
289        self.as_str()
290    }
291}
292
293impl PartialEq<&str> for Line<'_> {
294    fn eq(&self, other: &&str) -> bool {
295        self.as_str() == *other
296    }
297}
298
299impl PartialEq<Line<'_>> for &str {
300    fn eq(&self, other: &Line<'_>) -> bool {
301        *self == other.as_str()
302    }
303}
304
305/// The line ending style used in Python source code.
306/// See <https://docs.python.org/3/reference/lexical_analysis.html#physical-lines>
307#[derive(Debug, PartialEq, Eq, Copy, Clone)]
308pub enum LineEnding {
309    Lf,
310    Cr,
311    CrLf,
312}
313
314impl Default for LineEnding {
315    fn default() -> Self {
316        if cfg!(windows) {
317            LineEnding::CrLf
318        } else {
319            LineEnding::Lf
320        }
321    }
322}
323
324impl LineEnding {
325    pub const fn as_str(&self) -> &'static str {
326        match self {
327            LineEnding::Lf => "\n",
328            LineEnding::CrLf => "\r\n",
329            LineEnding::Cr => "\r",
330        }
331    }
332
333    #[expect(clippy::len_without_is_empty)]
334    pub const fn len(&self) -> usize {
335        match self {
336            LineEnding::Lf | LineEnding::Cr => 1,
337            LineEnding::CrLf => 2,
338        }
339    }
340
341    pub const fn text_len(&self) -> TextSize {
342        match self {
343            LineEnding::Lf | LineEnding::Cr => TextSize::new(1),
344            LineEnding::CrLf => TextSize::new(2),
345        }
346    }
347}
348
349impl Deref for LineEnding {
350    type Target = str;
351
352    fn deref(&self) -> &Self::Target {
353        self.as_str()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use text_size::TextSize;
360
361    use super::{Line, UniversalNewlineIterator};
362
363    #[test]
364    fn universal_newlines_empty_str() {
365        let lines: Vec<_> = UniversalNewlineIterator::from("").collect();
366        assert_eq!(lines, Vec::<Line<'_>>::new());
367
368        let lines: Vec<_> = UniversalNewlineIterator::from("").rev().collect();
369        assert_eq!(lines, Vec::<Line<'_>>::new());
370    }
371
372    #[test]
373    fn universal_newlines_forward() {
374        let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop").collect();
375        assert_eq!(
376            lines,
377            vec![
378                Line::new("foo\n", TextSize::from(0)),
379                Line::new("bar\n", TextSize::from(4)),
380                Line::new("\r\n", TextSize::from(8)),
381                Line::new("baz\r", TextSize::from(10)),
382                Line::new("bop", TextSize::from(14)),
383            ]
384        );
385
386        let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n").collect();
387        assert_eq!(
388            lines,
389            vec![
390                Line::new("foo\n", TextSize::from(0)),
391                Line::new("bar\n", TextSize::from(4)),
392                Line::new("\r\n", TextSize::from(8)),
393                Line::new("baz\r", TextSize::from(10)),
394                Line::new("bop\n", TextSize::from(14)),
395            ]
396        );
397
398        let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n\n").collect();
399        assert_eq!(
400            lines,
401            vec![
402                Line::new("foo\n", TextSize::from(0)),
403                Line::new("bar\n", TextSize::from(4)),
404                Line::new("\r\n", TextSize::from(8)),
405                Line::new("baz\r", TextSize::from(10)),
406                Line::new("bop\n", TextSize::from(14)),
407                Line::new("\n", TextSize::from(18)),
408            ]
409        );
410    }
411
412    #[test]
413    fn universal_newlines_backwards() {
414        let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop")
415            .rev()
416            .collect();
417        assert_eq!(
418            lines,
419            vec![
420                Line::new("bop", TextSize::from(14)),
421                Line::new("baz\r", TextSize::from(10)),
422                Line::new("\r\n", TextSize::from(8)),
423                Line::new("bar\n", TextSize::from(4)),
424                Line::new("foo\n", TextSize::from(0)),
425            ]
426        );
427
428        let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\nbaz\rbop\n")
429            .rev()
430            .map(|line| line.as_str())
431            .collect();
432
433        assert_eq!(
434            lines,
435            vec![
436                Line::new("bop\n", TextSize::from(13)),
437                Line::new("baz\r", TextSize::from(9)),
438                Line::new("\n", TextSize::from(8)),
439                Line::new("bar\n", TextSize::from(4)),
440                Line::new("foo\n", TextSize::from(0)),
441            ]
442        );
443    }
444
445    #[test]
446    fn universal_newlines_mixed() {
447        let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop");
448
449        assert_eq!(
450            lines.next_back(),
451            Some(Line::new("bop", TextSize::from(14)))
452        );
453        assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0))));
454        assert_eq!(
455            lines.next_back(),
456            Some(Line::new("baz\r", TextSize::from(10)))
457        );
458        assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4))));
459        assert_eq!(
460            lines.next_back(),
461            Some(Line::new("\r\n", TextSize::from(8)))
462        );
463        assert_eq!(lines.next(), None);
464    }
465}