use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use crate::line_map::LineMap;
use crate::span::FileSpan;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(u32);
impl FileId {
pub const SYNTHETIC: FileId = FileId(u32::MAX);
#[inline]
pub const fn to_u32(self) -> u32 {
self.0
}
}
impl std::fmt::Debug for FileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if *self == Self::SYNTHETIC {
write!(f, "FileId(<synthetic>)")
} else {
write!(f, "FileId({})", self.0)
}
}
}
#[derive(Clone)]
pub struct SourceFile {
id: FileId,
path: PathBuf,
text: String,
line_map: LineMap,
}
impl SourceFile {
#[inline]
pub fn id(&self) -> FileId {
self.id
}
#[inline]
pub fn path(&self) -> &Path {
&self.path
}
#[inline]
pub fn text(&self) -> &str {
&self.text
}
#[inline]
pub fn line_map(&self) -> &LineMap {
&self.line_map
}
pub fn full_span(&self) -> FileSpan {
FileSpan::new(self.id, crate::span::Span::new(0, self.text.len() as u32))
}
}
#[derive(Default)]
pub struct SourceMap {
files: RwLock<Vec<Arc<SourceFile>>>,
}
impl SourceMap {
pub fn new() -> SourceMap {
SourceMap::default()
}
pub fn intern(&self, path: impl Into<PathBuf>, text: impl Into<String>) -> FileId {
let path = path.into();
let text = text.into();
let line_map = LineMap::new(&text);
let mut files = self.files.write().unwrap();
let id = u32::try_from(files.len()).expect("more than 2^32 source files");
assert!(id != FileId::SYNTHETIC.to_u32(), "file id space exhausted");
files.push(Arc::new(SourceFile {
id: FileId(id),
path,
text,
line_map,
}));
FileId(id)
}
pub fn len(&self) -> usize {
self.files.read().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.files.read().unwrap().is_empty()
}
pub fn get(&self, id: FileId) -> Option<FileView> {
if id == FileId::SYNTHETIC {
return None;
}
let files = self.files.read().unwrap();
let file = Arc::clone(files.get(id.to_u32() as usize)?);
Some(FileView { file })
}
}
#[derive(Clone)]
pub struct FileView {
file: Arc<SourceFile>,
}
impl std::ops::Deref for FileView {
type Target = SourceFile;
fn deref(&self) -> &SourceFile {
&self.file
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_assigns_sequential_ids() {
let map = SourceMap::new();
let a = map.intern("a.px", "first");
let b = map.intern("b.px", "second");
assert_eq!(a.to_u32(), 0);
assert_eq!(b.to_u32(), 1);
assert_eq!(map.len(), 2);
}
#[test]
fn intern_same_path_yields_distinct_ids() {
let map = SourceMap::new();
let first = map.intern("dup.px", "one");
let second = map.intern("dup.px", "two");
assert_ne!(first, second, "each intern is a distinct snapshot");
}
#[test]
fn get_returns_interned_file() {
let map = SourceMap::new();
let id = map.intern("day.px", "out(1)\n");
let view = map.get(id).expect("file was just interned");
assert_eq!(view.path(), Path::new("day.px"));
assert_eq!(view.text(), "out(1)\n");
assert_eq!(view.id(), id);
}
#[test]
fn synthetic_and_unknown_ids_return_none() {
let map = SourceMap::new();
assert!(map.get(FileId::SYNTHETIC).is_none());
assert!(map.get(FileId(0)).is_none()); }
#[test]
fn full_span_covers_whole_file() {
let map = SourceMap::new();
let id = map.intern("f.px", "abc");
let view = map.get(id).unwrap();
let span = view.full_span();
assert_eq!(span.file, id);
assert_eq!(span.span.start().to_u32(), 0);
assert_eq!(span.span.end().to_u32(), 3);
}
#[test]
fn empty_map_reports_empty() {
let map = SourceMap::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn regression_file_view_remains_valid_when_more_files_are_interned() {
let map = SourceMap::new();
let first = map.intern("first.px", "stable");
let view = map.get(first).expect("first file exists");
for i in 0..4_096 {
map.intern(format!("later-{i}.px"), format!("revision {i}"));
}
assert_eq!(view.text(), "stable");
assert_eq!(view.path(), Path::new("first.px"));
}
}