use crate::span::Span;
use std::{cell::RefCell, fmt, path::PathBuf, rc::Rc};
#[derive(Default)]
pub struct SourceMap {
inner: RefCell<SourceMapInner>,
}
#[derive(Default)]
struct SourceMapInner {
used_address_space: u32,
source_files: Vec<Rc<SourceFile>>,
}
impl SourceMap {
pub fn new_source(&self, source: &str, name: FileName) -> Rc<SourceFile> {
let len = u32::try_from(source.len()).unwrap();
let mut inner = self.inner.borrow_mut();
let start_pos = inner.try_allocate_address_space(len).unwrap();
let source_file = Rc::new(SourceFile::new(name, source.to_owned(), start_pos));
inner.source_files.push(source_file.clone());
source_file
}
fn find_source_file_index(&self, pos: u32) -> Option<usize> {
self.inner
.borrow()
.source_files
.binary_search_by_key(&pos, |file| file.absolute_start)
.map_or_else(|p| p.checked_sub(1), Some)
}
pub fn find_source_file(&self, pos: u32) -> Option<Rc<SourceFile>> {
Some(self.inner.borrow().source_files[self.find_source_file_index(pos)?].clone())
}
pub fn source_file_by_filename(&self, filename: &FileName) -> Option<Rc<SourceFile>> {
self.inner.borrow().source_files.iter().find(|source_file| &source_file.name == filename).cloned()
}
pub fn contents_of_span(&self, span: Span) -> Option<String> {
let source_file1 = self.find_source_file(span.lo)?;
let source_file2 = self.find_source_file(span.hi)?;
assert_eq!(source_file1.absolute_start, source_file2.absolute_start);
Some(source_file1.contents_of_span(span).to_string())
}
}
impl SourceMapInner {
fn try_allocate_address_space(&mut self, size: u32) -> Option<u32> {
let current = self.used_address_space;
self.used_address_space = current.checked_add(size)?.checked_add(1)?;
Some(current)
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum FileName {
Real(PathBuf),
Custom(String),
}
impl fmt::Display for FileName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Real(x) if is_color() => x.display().fmt(f),
Self::Real(_) => Ok(()),
Self::Custom(x) => f.write_str(x),
}
}
}
pub fn is_color() -> bool {
std::env::var("NOCOLOR").unwrap_or_default().trim().is_empty()
}
pub struct SourceFile {
pub name: FileName,
pub src: String,
pub absolute_start: u32,
pub absolute_end: u32,
}
impl SourceFile {
fn new(name: FileName, src: String, absolute_start: u32) -> Self {
let absolute_end = absolute_start + src.len() as u32;
Self { name, src, absolute_start, absolute_end }
}
pub fn relative_offset(&self, absolute_offset: u32) -> u32 {
assert!(self.absolute_start <= absolute_offset);
assert!(absolute_offset <= self.absolute_end);
absolute_offset - self.absolute_start
}
pub fn contents_of_span(&self, span: Span) -> &str {
let start = self.relative_offset(span.lo);
let end = self.relative_offset(span.hi);
&self.src[start as usize..end as usize]
}
pub fn line_col(&self, absolute_offset: u32) -> (u32, u32) {
let relative_offset = self.relative_offset(absolute_offset);
let mut current_offset = 0u32;
for (i, line) in self.src.split('\n').enumerate() {
let end_of_line = current_offset + line.len() as u32;
if relative_offset <= end_of_line {
let chars = self.src[current_offset as usize..relative_offset as usize].chars().count();
return (i as u32, chars as u32);
}
current_offset = end_of_line + 1;
}
panic!("Can't happen.");
}
pub fn line_contents(&self, span: Span) -> LineContents<'_> {
let start = self.relative_offset(span.lo) as usize;
let end = self.relative_offset(span.hi) as usize;
let line_start = if self.src.get(start..).is_some_and(|s| s.starts_with('\n')) {
start
} else {
self.src[..start].rfind('\n').map(|i| i + 1).unwrap_or(0)
};
let line_end = self.src[end..].find('\n').map(|x| x + end).unwrap_or(self.src.len());
LineContents {
line: self.src[..line_start].lines().count(),
contents: &self.src[line_start..line_end],
start: start.saturating_sub(line_start),
end: end.saturating_sub(line_start),
}
}
}
pub struct LineContents<'a> {
pub contents: &'a str,
pub line: usize,
pub start: usize,
pub end: usize,
}
impl fmt::Display for LineContents<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const INDENT: &str = " ";
let mut current_underline = String::new();
let mut line = self.line;
let mut line_beginning = true;
let mut underline_started = false;
writeln!(f, "{INDENT} |")?;
for (i, c) in self.contents.chars().enumerate() {
if line_beginning {
write!(
f,
"{line:width$} | ",
line = line + 1,
width = INDENT.len()
)?;
}
if c == '\n' {
writeln!(f)?;
let underline = current_underline.trim_end();
if !underline.is_empty() {
writeln!(f, "{INDENT} | {underline}")?;
}
underline_started = false;
current_underline.clear();
line += 1;
line_beginning = true;
} else {
line_beginning = false;
if c != '\r' {
write!(f, "{c}")?;
if self.start <= i && i < self.end && (underline_started || !c.is_whitespace()) {
underline_started = true;
current_underline.push('^');
} else {
current_underline.push(' ');
}
}
}
}
if self.start == self.end
&& current_underline.chars().all(|c| c == ' ')
&& self.start <= current_underline.len()
{
current_underline.truncate(self.start);
current_underline.push('^');
}
let underline = current_underline.trim_end();
if !underline.is_empty() {
writeln!(f, "\n{INDENT} | {underline}")?;
}
Ok(())
}
}
pub struct LineCol {
pub source_file: Rc<SourceFile>,
pub line: u32,
pub col: u32,
}