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}