1use std::path::{Path, PathBuf};
9use std::sync::{Arc, RwLock};
10
11use crate::line_map::LineMap;
12use crate::span::FileSpan;
13
14#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct FileId(u32);
21
22impl FileId {
23 pub const SYNTHETIC: FileId = FileId(u32::MAX);
28
29 #[inline]
30 pub const fn to_u32(self) -> u32 {
31 self.0
32 }
33}
34
35impl std::fmt::Debug for FileId {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 if *self == Self::SYNTHETIC {
38 write!(f, "FileId(<synthetic>)")
39 } else {
40 write!(f, "FileId({})", self.0)
41 }
42 }
43}
44
45#[derive(Clone)]
48pub struct SourceFile {
49 id: FileId,
50 path: PathBuf,
51 text: String,
52 line_map: LineMap,
53}
54
55impl SourceFile {
56 #[inline]
57 pub fn id(&self) -> FileId {
58 self.id
59 }
60
61 #[inline]
62 pub fn path(&self) -> &Path {
63 &self.path
64 }
65
66 #[inline]
67 pub fn text(&self) -> &str {
68 &self.text
69 }
70
71 #[inline]
72 pub fn line_map(&self) -> &LineMap {
73 &self.line_map
74 }
75
76 pub fn full_span(&self) -> FileSpan {
78 FileSpan::new(self.id, crate::span::Span::new(0, self.text.len() as u32))
79 }
80}
81
82#[derive(Default)]
89pub struct SourceMap {
90 files: RwLock<Vec<Arc<SourceFile>>>,
95}
96
97impl SourceMap {
98 pub fn new() -> SourceMap {
100 SourceMap::default()
101 }
102
103 pub fn intern(&self, path: impl Into<PathBuf>, text: impl Into<String>) -> FileId {
110 let path = path.into();
111 let text = text.into();
112 let line_map = LineMap::new(&text);
113
114 let mut files = self.files.write().unwrap();
115 let id = u32::try_from(files.len()).expect("more than 2^32 source files");
118 assert!(id != FileId::SYNTHETIC.to_u32(), "file id space exhausted");
119
120 files.push(Arc::new(SourceFile {
121 id: FileId(id),
122 path,
123 text,
124 line_map,
125 }));
126 FileId(id)
127 }
128
129 pub fn len(&self) -> usize {
131 self.files.read().unwrap().len()
132 }
133
134 pub fn is_empty(&self) -> bool {
136 self.files.read().unwrap().is_empty()
137 }
138
139 pub fn get(&self, id: FileId) -> Option<FileView> {
144 if id == FileId::SYNTHETIC {
145 return None;
146 }
147 let files = self.files.read().unwrap();
148 let file = Arc::clone(files.get(id.to_u32() as usize)?);
149 Some(FileView { file })
150 }
151}
152
153#[derive(Clone)]
159pub struct FileView {
160 file: Arc<SourceFile>,
161}
162
163impl std::ops::Deref for FileView {
164 type Target = SourceFile;
165 fn deref(&self) -> &SourceFile {
166 &self.file
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn intern_assigns_sequential_ids() {
176 let map = SourceMap::new();
177 let a = map.intern("a.px", "first");
178 let b = map.intern("b.px", "second");
179 assert_eq!(a.to_u32(), 0);
180 assert_eq!(b.to_u32(), 1);
181 assert_eq!(map.len(), 2);
182 }
183
184 #[test]
185 fn intern_same_path_yields_distinct_ids() {
186 let map = SourceMap::new();
187 let first = map.intern("dup.px", "one");
188 let second = map.intern("dup.px", "two");
189 assert_ne!(first, second, "each intern is a distinct snapshot");
190 }
191
192 #[test]
193 fn get_returns_interned_file() {
194 let map = SourceMap::new();
195 let id = map.intern("day.px", "out(1)\n");
196 let view = map.get(id).expect("file was just interned");
197 assert_eq!(view.path(), Path::new("day.px"));
198 assert_eq!(view.text(), "out(1)\n");
199 assert_eq!(view.id(), id);
200 }
201
202 #[test]
203 fn synthetic_and_unknown_ids_return_none() {
204 let map = SourceMap::new();
205 assert!(map.get(FileId::SYNTHETIC).is_none());
206 assert!(map.get(FileId(0)).is_none()); }
208
209 #[test]
210 fn full_span_covers_whole_file() {
211 let map = SourceMap::new();
212 let id = map.intern("f.px", "abc");
213 let view = map.get(id).unwrap();
214 let span = view.full_span();
215 assert_eq!(span.file, id);
216 assert_eq!(span.span.start().to_u32(), 0);
217 assert_eq!(span.span.end().to_u32(), 3);
218 }
219
220 #[test]
221 fn empty_map_reports_empty() {
222 let map = SourceMap::new();
223 assert!(map.is_empty());
224 assert_eq!(map.len(), 0);
225 }
226
227 #[test]
231 fn regression_file_view_remains_valid_when_more_files_are_interned() {
232 let map = SourceMap::new();
233 let first = map.intern("first.px", "stable");
234 let view = map.get(first).expect("first file exists");
235
236 for i in 0..4_096 {
240 map.intern(format!("later-{i}.px"), format!("revision {i}"));
241 }
242
243 assert_eq!(view.text(), "stable");
244 assert_eq!(view.path(), Path::new("first.px"));
245 }
246}