Skip to main content

ruff_source_file/
lib.rs

1use std::cmp::Ordering;
2use std::fmt::{Debug, Display, Formatter};
3use std::hash::Hash;
4use std::sync::{Arc, OnceLock};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use ruff_text_size::{Ranged, TextSize};
10
11pub use crate::line_index::{LineIndex, OneIndexed, PositionEncoding};
12pub use crate::line_ranges::LineRanges;
13pub use crate::newlines::{
14    Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, UniversalNewlines,
15    find_newline,
16};
17
18mod line_index;
19mod line_ranges;
20mod newlines;
21
22/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`LineColumn`].
23#[derive(Debug)]
24pub struct SourceCode<'src, 'index> {
25    text: &'src str,
26    index: &'index LineIndex,
27}
28
29impl<'src, 'index> SourceCode<'src, 'index> {
30    pub fn new(content: &'src str, index: &'index LineIndex) -> Self {
31        Self {
32            text: content,
33            index,
34        }
35    }
36
37    /// Computes the one indexed line and column numbers for `offset`, skipping any potential BOM.
38    #[inline]
39    pub fn line_column(&self, offset: TextSize) -> LineColumn {
40        self.index.line_column(offset, self.text)
41    }
42
43    #[inline]
44    pub fn source_location(
45        &self,
46        offset: TextSize,
47        position_encoding: PositionEncoding,
48    ) -> SourceLocation {
49        self.index
50            .source_location(offset, self.text, position_encoding)
51    }
52
53    #[inline]
54    pub fn line_index(&self, offset: TextSize) -> OneIndexed {
55        self.index.line_index(offset)
56    }
57
58    /// Take the source code between the given [`ruff_text_size::TextRange`].
59    pub fn slice<T: Ranged>(&self, ranged: T) -> &'src str {
60        &self.text[ranged.range()]
61    }
62
63    pub fn line_start(&self, line: OneIndexed) -> TextSize {
64        self.index.line_start(line, self.text)
65    }
66
67    pub fn line_end(&self, line: OneIndexed) -> TextSize {
68        self.index.line_end(line, self.text)
69    }
70
71    pub fn line_end_exclusive(&self, line: OneIndexed) -> TextSize {
72        self.index.line_end_exclusive(line, self.text)
73    }
74
75    /// Returns the source text of the line with the given index
76    #[inline]
77    pub fn line_text(&self, index: OneIndexed) -> &'src str {
78        let range = self.index.line_range(index, self.text);
79        &self.text[range]
80    }
81
82    /// Returns the source text
83    pub fn text(&self) -> &'src str {
84        self.text
85    }
86
87    /// Returns the number of lines
88    #[inline]
89    pub fn line_count(&self) -> usize {
90        self.index.line_count()
91    }
92}
93
94impl PartialEq<Self> for SourceCode<'_, '_> {
95    fn eq(&self, other: &Self) -> bool {
96        self.text == other.text
97    }
98}
99
100impl Eq for SourceCode<'_, '_> {}
101
102/// A Builder for constructing a [`SourceFile`]
103pub struct SourceFileBuilder {
104    name: Box<str>,
105    code: Box<str>,
106    index: Option<LineIndex>,
107}
108
109impl SourceFileBuilder {
110    /// Creates a new builder for a file named `name`.
111    pub fn new<Name: Into<Box<str>>, Code: Into<Box<str>>>(name: Name, code: Code) -> Self {
112        Self {
113            name: name.into(),
114            code: code.into(),
115            index: None,
116        }
117    }
118
119    /// Consumes `self` and returns the [`SourceFile`].
120    pub fn finish(self) -> SourceFile {
121        let index = if let Some(index) = self.index {
122            OnceLock::from(index)
123        } else {
124            OnceLock::new()
125        };
126
127        SourceFile {
128            inner: Arc::new(SourceFileInner {
129                name: self.name,
130                code: self.code,
131                line_index: index,
132            }),
133        }
134    }
135}
136
137/// A source file that is identified by its name. Optionally stores the source code and [`LineIndex`].
138///
139/// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count.
140#[derive(Clone, Eq, PartialEq, Hash)]
141#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
142pub struct SourceFile {
143    inner: Arc<SourceFileInner>,
144}
145
146impl Debug for SourceFile {
147    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("SourceFile")
149            .field("name", &self.name())
150            .field("code", &self.source_text())
151            .finish()
152    }
153}
154
155impl SourceFile {
156    /// Returns the name of the source file (filename).
157    #[inline]
158    pub fn name(&self) -> &str {
159        &self.inner.name
160    }
161
162    pub fn to_source_code(&self) -> SourceCode<'_, '_> {
163        SourceCode {
164            text: self.source_text(),
165            index: self.index(),
166        }
167    }
168
169    pub fn index(&self) -> &LineIndex {
170        self.inner
171            .line_index
172            .get_or_init(|| LineIndex::from_source_text(self.source_text()))
173    }
174
175    /// Returns the source code.
176    #[inline]
177    pub fn source_text(&self) -> &str {
178        &self.inner.code
179    }
180}
181
182impl PartialOrd for SourceFile {
183    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
184        Some(self.cmp(other))
185    }
186}
187
188impl Ord for SourceFile {
189    fn cmp(&self, other: &Self) -> Ordering {
190        // Short circuit if these are the same source files
191        if Arc::ptr_eq(&self.inner, &other.inner) {
192            Ordering::Equal
193        } else {
194            self.inner.name.cmp(&other.inner.name)
195        }
196    }
197}
198
199#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
200struct SourceFileInner {
201    name: Box<str>,
202    code: Box<str>,
203    line_index: OnceLock<LineIndex>,
204}
205
206impl PartialEq for SourceFileInner {
207    fn eq(&self, other: &Self) -> bool {
208        self.name == other.name && self.code == other.code
209    }
210}
211
212impl Eq for SourceFileInner {}
213
214impl Hash for SourceFileInner {
215    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
216        self.name.hash(state);
217        self.code.hash(state);
218    }
219}
220
221/// The line and column of an offset in a source file.
222///
223/// See [`LineIndex::line_column`] for more information.
224#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
225#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
226pub struct LineColumn {
227    /// The line in the source text.
228    pub line: OneIndexed,
229    /// The column (UTF scalar values) relative to the start of the line except any
230    /// potential BOM on the first line.
231    pub column: OneIndexed,
232}
233
234impl Default for LineColumn {
235    fn default() -> Self {
236        Self {
237            line: OneIndexed::MIN,
238            column: OneIndexed::MIN,
239        }
240    }
241}
242
243impl Debug for LineColumn {
244    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245        f.debug_struct("LineColumn")
246            .field("line", &self.line.get())
247            .field("column", &self.column.get())
248            .finish()
249    }
250}
251
252impl std::fmt::Display for LineColumn {
253    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
254        write!(f, "{line}:{column}", line = self.line, column = self.column)
255    }
256}
257
258/// A position into a source file represented by the line number and the offset to that character relative to the start of that line.
259#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
260#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
261pub struct SourceLocation {
262    /// The line in the source text.
263    pub line: OneIndexed,
264    /// The offset from the start of the line to the character.
265    ///
266    /// This can be a byte offset, the number of UTF16 code points, or the UTF8 code units, depending on the
267    /// [`PositionEncoding`] used.
268    pub character_offset: OneIndexed,
269}
270
271impl Default for SourceLocation {
272    fn default() -> Self {
273        Self {
274            line: OneIndexed::MIN,
275            character_offset: OneIndexed::MIN,
276        }
277    }
278}
279
280#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
281pub enum SourceRow {
282    /// A row within a cell in a Jupyter Notebook.
283    Notebook { cell: OneIndexed, line: OneIndexed },
284    /// A row within a source file.
285    SourceFile { line: OneIndexed },
286}
287
288impl Display for SourceRow {
289    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
290        match self {
291            SourceRow::Notebook { cell, line } => write!(f, "cell {cell}, line {line}"),
292            SourceRow::SourceFile { line } => write!(f, "line {line}"),
293        }
294    }
295}