mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:09:22 +00:00

## Summary Resolves #14704. ## Test Plan `cargo nextest run` and `cargo insta test`. --------- Co-authored-by: Micha Reiser <micha@reiser.io>
396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
use crate::find_newline;
|
|
use memchr::{memchr2, memrchr2};
|
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
|
use std::ops::Add;
|
|
|
|
/// Extension trait for [`str`] that provides methods for working with ranges of lines.
|
|
pub trait LineRanges {
|
|
/// Computes the start position of the line of `offset`.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::TextSize;
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\rthird line";
|
|
///
|
|
/// assert_eq!(text.line_start(TextSize::from(0)), TextSize::from(0));
|
|
/// assert_eq!(text.line_start(TextSize::from(4)), TextSize::from(0));
|
|
///
|
|
/// assert_eq!(text.line_start(TextSize::from(14)), TextSize::from(11));
|
|
/// assert_eq!(text.line_start(TextSize::from(28)), TextSize::from(23));
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If `offset` is out of bounds.
|
|
fn line_start(&self, offset: TextSize) -> TextSize;
|
|
|
|
/// Computes the start position of the file contents: either the first byte, or the byte after
|
|
/// the BOM.
|
|
fn bom_start_offset(&self) -> TextSize;
|
|
|
|
/// Returns `true` if `offset` is at the start of a line.
|
|
fn is_at_start_of_line(&self, offset: TextSize) -> bool {
|
|
self.line_start(offset) == offset
|
|
}
|
|
|
|
/// Computes the offset that is right after the newline character that ends `offset`'s line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.full_line_end(TextSize::from(3)), TextSize::from(11));
|
|
/// assert_eq!(text.full_line_end(TextSize::from(14)), TextSize::from(24));
|
|
/// assert_eq!(text.full_line_end(TextSize::from(28)), TextSize::from(34));
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
///
|
|
/// If `offset` is passed the end of the content.
|
|
fn full_line_end(&self, offset: TextSize) -> TextSize;
|
|
|
|
/// Computes the offset that is right before the newline character that ends `offset`'s line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.line_end(TextSize::from(3)), TextSize::from(10));
|
|
/// assert_eq!(text.line_end(TextSize::from(14)), TextSize::from(22));
|
|
/// assert_eq!(text.line_end(TextSize::from(28)), TextSize::from(34));
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
///
|
|
/// If `offset` is passed the end of the content.
|
|
fn line_end(&self, offset: TextSize) -> TextSize;
|
|
|
|
/// Computes the range of this `offset`s line.
|
|
///
|
|
/// The range starts at the beginning of the line and goes up to, and including, the new line character
|
|
/// at the end of the line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.full_line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(11)));
|
|
/// assert_eq!(text.full_line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(24)));
|
|
/// assert_eq!(text.full_line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34)));
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If `offset` is out of bounds.
|
|
fn full_line_range(&self, offset: TextSize) -> TextRange {
|
|
TextRange::new(self.line_start(offset), self.full_line_end(offset))
|
|
}
|
|
|
|
/// Computes the range of this `offset`s line ending before the newline character.
|
|
///
|
|
/// The range starts at the beginning of the line and goes up to, but excluding, the new line character
|
|
/// at the end of the line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(10)));
|
|
/// assert_eq!(text.line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(22)));
|
|
/// assert_eq!(text.line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34)));
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If `offset` is out of bounds.
|
|
fn line_range(&self, offset: TextSize) -> TextRange {
|
|
TextRange::new(self.line_start(offset), self.line_end(offset))
|
|
}
|
|
|
|
/// Returns the text of the `offset`'s line.
|
|
///
|
|
/// The line includes the newline characters at the end of the line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.full_line_str(TextSize::from(3)), "First line\n");
|
|
/// assert_eq!(text.full_line_str(TextSize::from(14)), "second line\r\n");
|
|
/// assert_eq!(text.full_line_str(TextSize::from(28)), "third line");
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If `offset` is out of bounds.
|
|
fn full_line_str(&self, offset: TextSize) -> &str;
|
|
|
|
/// Returns the text of the `offset`'s line.
|
|
///
|
|
/// Excludes the newline characters at the end of the line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(text.line_str(TextSize::from(3)), "First line");
|
|
/// assert_eq!(text.line_str(TextSize::from(14)), "second line");
|
|
/// assert_eq!(text.line_str(TextSize::from(28)), "third line");
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If `offset` is out of bounds.
|
|
fn line_str(&self, offset: TextSize) -> &str;
|
|
|
|
/// Computes the range of all lines that this `range` covers.
|
|
///
|
|
/// The range starts at the beginning of the line at `range.start()` and goes up to, and including, the new line character
|
|
/// at the end of `range.ends()`'s line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(
|
|
/// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))),
|
|
/// TextRange::new(TextSize::from(0), TextSize::from(11))
|
|
/// );
|
|
/// assert_eq!(
|
|
/// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))),
|
|
/// TextRange::new(TextSize::from(0), TextSize::from(24))
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If the start or end of `range` is out of bounds.
|
|
fn full_lines_range(&self, range: TextRange) -> TextRange {
|
|
TextRange::new(
|
|
self.line_start(range.start()),
|
|
self.full_line_end(range.end()),
|
|
)
|
|
}
|
|
|
|
/// Computes the range of all lines that this `range` covers.
|
|
///
|
|
/// The range starts at the beginning of the line at `range.start()` and goes up to, but excluding, the new line character
|
|
/// at the end of `range.end()`'s line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(
|
|
/// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))),
|
|
/// TextRange::new(TextSize::from(0), TextSize::from(10))
|
|
/// );
|
|
/// assert_eq!(
|
|
/// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))),
|
|
/// TextRange::new(TextSize::from(0), TextSize::from(22))
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If the start or end of `range` is out of bounds.
|
|
fn lines_range(&self, range: TextRange) -> TextRange {
|
|
TextRange::new(self.line_start(range.start()), self.line_end(range.end()))
|
|
}
|
|
|
|
/// Returns true if the text of `range` contains any line break.
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert!(
|
|
/// !text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(5))),
|
|
/// );
|
|
/// assert!(
|
|
/// text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(14))),
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If the `range` is out of bounds.
|
|
fn contains_line_break(&self, range: TextRange) -> bool;
|
|
|
|
/// Returns the text of all lines that include `range`.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(
|
|
/// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))),
|
|
/// "First line"
|
|
/// );
|
|
/// assert_eq!(
|
|
/// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))),
|
|
/// "First line\nsecond line"
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If the start or end of `range` is out of bounds.
|
|
fn lines_str(&self, range: TextRange) -> &str;
|
|
|
|
/// Returns the text of all lines that include `range`.
|
|
///
|
|
/// Includes the newline characters of the last line.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// let text = "First line\nsecond line\r\nthird line";
|
|
///
|
|
/// assert_eq!(
|
|
/// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))),
|
|
/// "First line\n"
|
|
/// );
|
|
/// assert_eq!(
|
|
/// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))),
|
|
/// "First line\nsecond line\r\n"
|
|
/// );
|
|
/// ```
|
|
///
|
|
/// ## Panics
|
|
/// If the start or end of `range` is out of bounds.
|
|
fn full_lines_str(&self, range: TextRange) -> &str;
|
|
|
|
/// The number of lines `range` spans.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// # use ruff_text_size::{Ranged, TextRange};
|
|
/// # use ruff_source_file::LineRanges;
|
|
///
|
|
/// assert_eq!("a\nb".count_lines(TextRange::up_to(1.into())), 0);
|
|
/// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(3.into())), 1, "Up to the end of the second line");
|
|
/// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(4.into())), 2, "In between the line break characters");
|
|
/// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(5.into())), 2);
|
|
/// assert_eq!("Single line".count_lines(TextRange::up_to(13.into())), 0);
|
|
/// assert_eq!("out\nof\nbounds end".count_lines(TextRange::up_to(55.into())), 2);
|
|
/// ```
|
|
fn count_lines(&self, range: TextRange) -> u32 {
|
|
let mut count = 0;
|
|
let mut line_end = self.line_end(range.start());
|
|
|
|
loop {
|
|
let next_line_start = self.full_line_end(line_end);
|
|
|
|
// Reached the end of the string
|
|
if next_line_start == line_end {
|
|
break count;
|
|
}
|
|
|
|
// Range ends at the line boundary
|
|
if line_end >= range.end() {
|
|
break count;
|
|
}
|
|
|
|
count += 1;
|
|
|
|
line_end = self.line_end(next_line_start);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LineRanges for str {
|
|
fn line_start(&self, offset: TextSize) -> TextSize {
|
|
let bytes = self[TextRange::up_to(offset)].as_bytes();
|
|
if let Some(index) = memrchr2(b'\n', b'\r', bytes) {
|
|
// SAFETY: Safe because `index < offset`
|
|
TextSize::try_from(index).unwrap().add(TextSize::from(1))
|
|
} else {
|
|
self.bom_start_offset()
|
|
}
|
|
}
|
|
|
|
fn bom_start_offset(&self) -> TextSize {
|
|
if self.starts_with('\u{feff}') {
|
|
// Skip the BOM.
|
|
'\u{feff}'.text_len()
|
|
} else {
|
|
// Start of file.
|
|
TextSize::default()
|
|
}
|
|
}
|
|
|
|
fn full_line_end(&self, offset: TextSize) -> TextSize {
|
|
let slice = &self[usize::from(offset)..];
|
|
if let Some((index, line_ending)) = find_newline(slice) {
|
|
offset + TextSize::try_from(index).unwrap() + line_ending.text_len()
|
|
} else {
|
|
self.text_len()
|
|
}
|
|
}
|
|
|
|
fn line_end(&self, offset: TextSize) -> TextSize {
|
|
let slice = &self[offset.to_usize()..];
|
|
if let Some(index) = memchr2(b'\n', b'\r', slice.as_bytes()) {
|
|
offset + TextSize::try_from(index).unwrap()
|
|
} else {
|
|
self.text_len()
|
|
}
|
|
}
|
|
|
|
fn full_line_str(&self, offset: TextSize) -> &str {
|
|
&self[self.full_line_range(offset)]
|
|
}
|
|
|
|
fn line_str(&self, offset: TextSize) -> &str {
|
|
&self[self.line_range(offset)]
|
|
}
|
|
|
|
fn contains_line_break(&self, range: TextRange) -> bool {
|
|
memchr2(b'\n', b'\r', self[range].as_bytes()).is_some()
|
|
}
|
|
|
|
fn lines_str(&self, range: TextRange) -> &str {
|
|
&self[self.lines_range(range)]
|
|
}
|
|
|
|
fn full_lines_str(&self, range: TextRange) -> &str {
|
|
&self[self.full_lines_range(range)]
|
|
}
|
|
}
|