use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Ignored<T>(pub(crate) T);
impl<T> PartialEq for Ignored<T> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<T> Eq for Ignored<T> {}
impl<T> std::hash::Hash for Ignored<T> {
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub(crate) u32);
impl FileId {
pub const DUMMY: FileId = FileId(u32::MAX);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Span {
pub(crate) file: FileId,
pub(crate) start: u32,
pub(crate) end: u32,
}
impl Span {
pub const DUMMY: Span = Span {
file: FileId::DUMMY,
start: 0,
end: 0,
};
pub fn new(file: FileId, start: u32, end: u32) -> Self {
debug_assert!(start <= end, "span start must be <= end");
Self { file, start, end }
}
pub fn is_dummy(&self) -> bool {
self.file == FileId::DUMMY
}
pub fn merge(self, other: Span) -> Span {
assert_eq!(
self.file, other.file,
"cannot merge spans from different files"
);
Span {
file: self.file,
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
pub fn range(&self) -> std::ops::Range<usize> {
self.start as usize..self.end as usize
}
}
pub(crate) struct SourceFile {
path: PathBuf,
text: String,
line_starts: Vec<u32>,
}
impl SourceFile {
fn new(path: PathBuf, text: String) -> Self {
let line_starts = codespan_reporting::files::line_starts(&text)
.map(|n| n as u32)
.collect();
Self {
path,
text,
line_starts,
}
}
}
#[derive(Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
pub fn new() -> Self {
Self::default()
}
pub fn load(&mut self, path: impl Into<PathBuf>) -> io::Result<FileId> {
let path = path.into();
let text = std::fs::read_to_string(&path)?;
Ok(self.add(path, text))
}
pub fn add(&mut self, path: PathBuf, text: String) -> FileId {
let id = FileId(self.files.len() as u32);
self.files.push(SourceFile::new(path, text));
id
}
pub(crate) fn file(&self, id: FileId) -> &SourceFile {
&self.files[id.0 as usize]
}
pub fn path(&self, id: FileId) -> &Path {
&self.file(id).path
}
pub fn text(&self, id: FileId) -> &str {
&self.file(id).text
}
pub fn snippet(&self, span: Span) -> &str {
if span.is_dummy() {
return "";
}
&self.file(span.file).text[span.range()]
}
pub fn line_col(&self, file: FileId, byte: u32) -> (u32, u32) {
let sf = self.file(file);
let line_idx = line_index_0(&sf.line_starts, byte);
let col = byte.saturating_sub(sf.line_starts[line_idx]);
(line_idx as u32 + 1, col + 1)
}
}
fn line_index_0(line_starts: &[u32], byte: u32) -> usize {
match line_starts.binary_search(&byte) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
}
}
impl<'a> codespan_reporting::files::Files<'a> for SourceMap {
type FileId = FileId;
type Name = String;
type Source = &'a str;
fn name(&'a self, id: FileId) -> Result<Self::Name, codespan_reporting::files::Error> {
Ok(self.path(id).display().to_string())
}
fn source(&'a self, id: FileId) -> Result<Self::Source, codespan_reporting::files::Error> {
Ok(self.text(id))
}
fn line_index(
&'a self,
id: FileId,
byte_index: usize,
) -> Result<usize, codespan_reporting::files::Error> {
Ok(line_index_0(&self.file(id).line_starts, byte_index as u32))
}
fn line_range(
&'a self,
id: FileId,
line_index: usize,
) -> Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
let sf = self.file(id);
let n = sf.line_starts.len();
if line_index >= n {
return Err(codespan_reporting::files::Error::LineTooLarge {
given: line_index,
max: n - 1,
});
}
let start = sf.line_starts[line_index] as usize;
let end = if line_index + 1 < n {
sf.line_starts[line_index + 1] as usize
} else {
sf.text.len()
};
Ok(start..end)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_same_file() {
let f = FileId(0);
let a = Span::new(f, 3, 7);
let b = Span::new(f, 5, 12);
assert_eq!(a.merge(b), Span::new(f, 3, 12));
assert_eq!(b.merge(a), Span::new(f, 3, 12));
}
#[test]
#[should_panic(expected = "different files")]
fn merge_different_files_panics() {
Span::new(FileId(0), 0, 1).merge(Span::new(FileId(1), 0, 1));
}
#[test]
fn line_col_multi_line() {
let mut sm = SourceMap::new();
let f = sm.add("x.dl".into(), "abc\ndefg\nhij".into());
assert_eq!(sm.line_col(f, 0), (1, 1));
assert_eq!(sm.line_col(f, 2), (1, 3));
assert_eq!(sm.line_col(f, 3), (1, 4));
assert_eq!(sm.line_col(f, 4), (2, 1));
assert_eq!(sm.line_col(f, 7), (2, 4));
assert_eq!(sm.line_col(f, 9), (3, 1));
assert_eq!(sm.line_col(f, 11), (3, 3));
}
#[test]
fn line_col_past_eof_clamps() {
let mut sm = SourceMap::new();
let f = sm.add("x.dl".into(), "abc".into());
let (line, col) = sm.line_col(f, 100);
assert_eq!(line, 1);
assert!(col >= 4);
}
#[test]
fn snippet_dummy_is_empty() {
let sm = SourceMap::new();
assert_eq!(sm.snippet(Span::DUMMY), "");
}
#[test]
#[allow(clippy::clone_on_copy)]
fn ignored_collapses_eq_and_hash_but_preserves_debug_clone() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let a = Ignored(42i32);
let b = Ignored(99i32);
assert_eq!(a, b); assert_eq!(a, a);
let hash_of = |x: &Ignored<i32>| {
let mut h = DefaultHasher::new();
x.hash(&mut h);
h.finish()
};
assert_eq!(hash_of(&a), hash_of(&b));
assert_eq!(format!("{a:?}"), "Ignored(42)");
let c = a;
let d = a.clone();
assert_eq!(c.0, 42);
assert_eq!(d.0, 42);
}
#[test]
fn load_reads_file_and_indexes_lines() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.dl");
std::fs::write(&path, "line one\nline two\n").unwrap();
let mut sm = SourceMap::new();
let f = sm.load(&path).unwrap();
assert_eq!(sm.text(f), "line one\nline two\n");
assert_eq!(sm.line_col(f, 9), (2, 1));
}
}