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::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
116#[cfg(test)]
117mod tests {
118 extern crate alloc;
119 use alloc::boxed::Box;
120
121 use super::*;
122
123 fn boxed(s: &str) -> Box<str> {
124 Box::from(s)
125 }
126
127 #[test]
128 fn test_accessors_return_stored_values() {
129 let file = SourceFile::new(boxed("name"), boxed("body"), Span::new(4, 8));
130 assert_eq!(file.name(), "name");
131 assert_eq!(file.text(), "body");
132 assert_eq!(file.span(), Span::new(4, 8));
133 }
134
135 #[test]
136 fn test_empty_text_has_zero_width_span() {
137 let file = SourceFile::new(boxed("empty"), boxed(""), Span::empty(12));
138 assert_eq!(file.text(), "");
139 assert!(file.span().is_empty());
140 }
141}