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` lookup on it is sub-linear.
121    ///
122    /// Prefer this over [`SourceMap::line_col`](crate::SourceMap::line_col) when
123    /// resolving several positions within one source — that convenience method
124    /// builds a fresh index per call, whereas this builds it once.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use source_lang::{BytePos, LineCol, SourceMap};
130    ///
131    /// let mut map = SourceMap::new();
132    /// let id = map.add("m.rs", "let x = 1;\nlet y = 2;").expect("fits");
133    /// let index = map.source(id).unwrap().line_index();
134    ///
135    /// // Resolve as many local positions as needed against the one index.
136    /// assert_eq!(index.line_col(BytePos::new(0)), LineCol::new(1, 1));
137    /// assert_eq!(index.line_col(BytePos::new(11)), LineCol::new(2, 1));
138    /// assert_eq!(index.line_count(), 2);
139    /// ```
140    #[inline]
141    #[must_use]
142    pub fn line_index(&self) -> LineIndex<'_> {
143        LineIndex::new(&self.text)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    extern crate alloc;
150    use alloc::boxed::Box;
151
152    use super::*;
153
154    fn boxed(s: &str) -> Box<str> {
155        Box::from(s)
156    }
157
158    #[test]
159    fn test_accessors_return_stored_values() {
160        let file = SourceFile::new(boxed("name"), boxed("body"), Span::new(4, 8));
161        assert_eq!(file.name(), "name");
162        assert_eq!(file.text(), "body");
163        assert_eq!(file.span(), Span::new(4, 8));
164    }
165
166    #[test]
167    fn test_empty_text_has_zero_width_span() {
168        let file = SourceFile::new(boxed("empty"), boxed(""), Span::empty(12));
169        assert_eq!(file.text(), "");
170        assert!(file.span().is_empty());
171    }
172}