Skip to main content

source_lang/
file.rs

1//! One stored source: its name, its text, and its place in the global space.
2
3use alloc::boxed::Box;
4
5use span_lang::{LineIndex, Span};
6
7/// A single source held by a [`SourceMap`](crate::SourceMap): a display name, the
8/// owned source text, and the half-open [`Span`] the text occupies in the map's
9/// global position space.
10///
11/// The map owns the text so that everything above it can borrow `&str` for the
12/// life of the map without re-reading or copying — a line index, a lexer, a
13/// diagnostic renderer all read through this borrow. The [`span`](SourceFile::span)
14/// is the file's footprint in the shared coordinate space: its `start` is the
15/// global offset of the first byte, and a local offset within the file plus that
16/// `start` is the corresponding global position.
17///
18/// Construction is internal; a `SourceFile` only ever comes from
19/// [`SourceMap::add`](crate::SourceMap::add) or
20/// [`SourceMap::source`](crate::SourceMap::source), so its span is always
21/// consistent with the map that produced it.
22///
23/// # Examples
24///
25/// ```
26/// use source_lang::SourceMap;
27///
28/// let mut map = SourceMap::new();
29/// let id = map.add("greeting.txt", "hello").expect("fits");
30/// let file = map.source(id).expect("just added");
31///
32/// assert_eq!(file.name(), "greeting.txt");
33/// assert_eq!(file.text(), "hello");
34/// assert_eq!(file.span().len(), 5);
35/// ```
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct SourceFile {
38    name: Box<str>,
39    text: Box<str>,
40    span: Span,
41}
42
43impl SourceFile {
44    /// Assembles a stored source. Internal: the span must match the slot the map
45    /// assigned, so only the map constructs one.
46    #[inline]
47    pub(crate) const fn new(name: Box<str>, text: Box<str>, span: Span) -> Self {
48        Self { name, text, span }
49    }
50
51    /// Returns the source's display name — the path or label it was added under.
52    ///
53    /// This is purely a label for diagnostics; the map does not interpret it, so
54    /// two sources may share a name and still be distinct entries.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use source_lang::SourceMap;
60    ///
61    /// let mut map = SourceMap::new();
62    /// let id = map.add("src/main.rs", "fn main() {}").expect("fits");
63    /// assert_eq!(map.source(id).unwrap().name(), "src/main.rs");
64    /// ```
65    #[inline]
66    #[must_use]
67    pub fn name(&self) -> &str {
68        &self.name
69    }
70
71    /// Returns the source text, borrowed for the life of the map.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use source_lang::SourceMap;
77    ///
78    /// let mut map = SourceMap::new();
79    /// let id = map.add("note.txt", "first line\nsecond line").expect("fits");
80    /// let text = map.source(id).unwrap().text();
81    /// assert_eq!(text.lines().count(), 2);
82    /// ```
83    #[inline]
84    #[must_use]
85    pub fn text(&self) -> &str {
86        &self.text
87    }
88
89    /// Returns the file's half-open range in the map's global position space.
90    ///
91    /// `span().start()` is the global offset of the file's first byte; the file
92    /// covers `start..start + text().len()`. Subtracting `start` from a global
93    /// position that falls in this range gives the local offset into
94    /// [`text`](SourceFile::text) — which is exactly what
95    /// [`SourceMap::locate`](crate::SourceMap::locate) returns.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use source_lang::SourceMap;
101    ///
102    /// let mut map = SourceMap::new();
103    /// let first = map.add("a.txt", "abc").expect("fits");   // global 0..3
104    /// let second = map.add("b.txt", "de").expect("fits");   // global 3..5
105    ///
106    /// assert_eq!(map.source(first).unwrap().span().start().to_u32(), 0);
107    /// assert_eq!(map.source(second).unwrap().span().start().to_u32(), 3);
108    /// ```
109    #[inline]
110    #[must_use]
111    pub const fn span(&self) -> Span {
112        self.span
113    }
114
115    /// Builds a reusable line index over this source's text.
116    ///
117    /// The returned [`LineIndex`] borrows the source for as long as the
118    /// [`SourceFile`] is borrowed, so it can be kept and queried many times
119    /// without re-scanning. Building it is the only `O(text len)` step; each
120    /// `line_col` / `offset` / `line_span` lookup on it is sub-linear.
121    ///
122    /// For plain line/column resolution, [`SourceMap::line_col`](crate::SourceMap::line_col)
123    /// needs no index at all: since 1.0.1 it answers from a line table the map
124    /// computes once when the source is added. Take a `LineIndex` when you need
125    /// what only it offers (the inverse [`offset`](LineIndex::offset) lookup or
126    /// [`line_span`](LineIndex::line_span)) and keep it rather than rebuilding it
127    /// per query: each call to this method scans the text again.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use source_lang::{BytePos, LineCol, SourceMap};
133    ///
134    /// let mut map = SourceMap::new();
135    /// let id = map.add("m.rs", "let x = 1;\nlet y = 2;").expect("fits");
136    /// let index = map.source(id).unwrap().line_index();
137    ///
138    /// // Resolve as many local positions as needed against the one index.
139    /// assert_eq!(index.line_col(BytePos::new(0)), LineCol::new(1, 1));
140    /// assert_eq!(index.line_col(BytePos::new(11)), LineCol::new(2, 1));
141    /// assert_eq!(index.line_count(), 2);
142    /// ```
143    #[inline]
144    #[must_use]
145    pub fn line_index(&self) -> LineIndex<'_> {
146        LineIndex::new(&self.text)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    extern crate alloc;
153    use alloc::boxed::Box;
154
155    use super::*;
156
157    fn boxed(s: &str) -> Box<str> {
158        Box::from(s)
159    }
160
161    #[test]
162    fn test_accessors_return_stored_values() {
163        let file = SourceFile::new(boxed("name"), boxed("body"), Span::new(4, 8));
164        assert_eq!(file.name(), "name");
165        assert_eq!(file.text(), "body");
166        assert_eq!(file.span(), Span::new(4, 8));
167    }
168
169    #[test]
170    fn test_empty_text_has_zero_width_span() {
171        let file = SourceFile::new(boxed("empty"), boxed(""), Span::empty(12));
172        assert_eq!(file.text(), "");
173        assert!(file.span().is_empty());
174    }
175}