use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FileId(pub(crate) u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Span {
pub file: FileId,
pub start: u32,
pub end: u32,
}
impl Span {
pub(crate) fn new(file: FileId, start: usize, end: usize) -> Self {
Span {
file,
start: start as u32,
end: end as u32,
}
}
pub fn to(self, other: Span) -> Span {
if self.file == other.file {
Span {
file: self.file,
start: self.start.min(other.start),
end: self.end.max(other.end),
}
} else {
self
}
}
}
#[derive(Debug)]
pub struct SourceFile {
pub name: PathBuf,
pub text: String,
line_starts: std::cell::OnceCell<Vec<u32>>,
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Location {
pub file: PathBuf,
pub line: u32,
pub column: u32,
}
impl SourceMap {
pub fn new() -> Self {
SourceMap::default()
}
pub(crate) fn add(&mut self, name: impl Into<PathBuf>, text: String) -> Option<FileId> {
if text.len() >= u32::MAX as usize || self.files.len() >= u32::MAX as usize {
return None;
}
let id = FileId(self.files.len() as u32);
self.files.push(SourceFile {
name: name.into(),
text,
line_starts: std::cell::OnceCell::new(),
});
Some(id)
}
pub fn file(&self, id: FileId) -> &SourceFile {
&self.files[id.0 as usize]
}
pub fn snippet(&self, span: Span) -> &str {
let text = &self.file(span.file).text;
text.get(span.start as usize..span.end as usize)
.unwrap_or("")
}
pub fn locate(&self, span: Span) -> Location {
let file = self.file(span.file);
let starts = file.line_starts.get_or_init(|| {
let mut v = vec![0u32];
for (i, b) in file.text.bytes().enumerate() {
if b == b'\n' {
v.push(i as u32 + 1);
}
}
v
});
let line = match starts.binary_search(&span.start) {
Ok(i) => i,
Err(i) => i - 1,
};
Location {
file: file.name.clone(),
line: line as u32 + 1,
column: span.start - starts[line] + 1,
}
}
}
impl SourceFile {
pub fn path(&self) -> &Path {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locate_lines_and_columns() {
let mut map = SourceMap::new();
let id = map.add("t.cir", "title\nr1 1 0 1k\n.end\n".into()).unwrap();
let loc = map.locate(Span::new(id, 6, 8));
assert_eq!(loc.line, 2);
assert_eq!(loc.column, 1);
let loc = map.locate(Span::new(id, 9, 10));
assert_eq!(loc.line, 2);
assert_eq!(loc.column, 4);
assert_eq!(map.snippet(Span::new(id, 6, 8)), "r1");
}
#[test]
fn locate_at_file_start_and_end() {
let mut map = SourceMap::new();
let id = map.add("t.cir", "a\nb".into()).unwrap();
assert_eq!(map.locate(Span::new(id, 0, 1)).line, 1);
let loc = map.locate(Span::new(id, 2, 3));
assert_eq!((loc.line, loc.column), (2, 1));
}
}