use std::{
collections::HashMap,
fmt::Debug,
num::NonZeroU32,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use crate::{Diagnostic, util};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileId(NonZeroU32);
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Source {
id: FileId,
path: PathBuf,
contents: Arc<str>,
line_offsets: Arc<[usize]>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SourceList {
ids: HashMap<PathBuf, FileId>,
sources: HashMap<FileId, Source>,
}
pub(crate) struct SourceLoader {
sources: SourceList,
resolver: Box<dyn SourceResolver>,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SourceMap {
offsets: Vec<(Range<usize>, (FileId, usize))>,
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("Failed to load source at '{}': '{cause}'", Path::new(.path.as_os_str()).display())]
pub struct SourceLoadError {
cause: Arc<str>,
path: PathBuf,
}
pub trait SourceResolver {
fn get_contents(&self, path: &Path) -> Result<Arc<str>, SourceLoadError>;
fn resolve_raw_path(&self, path: &Path, _included_from: Option<&Path>) -> PathBuf {
path.to_path_buf()
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf, SourceLoadError> {
Ok(path.to_owned())
}
#[doc(hidden)]
fn resolve(&self, path: &Path) -> Result<Source, SourceLoadError> {
let contents = self.get_contents(path)?;
Ok(Source::new(path.to_owned(), contents))
}
#[doc(hidden)]
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
impl std::fmt::Debug for dyn SourceResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.type_name().fmt(f)
}
}
impl<F> SourceResolver for F
where
F: Fn(&Path) -> Result<Arc<str>, SourceLoadError>,
{
fn get_contents(&self, path: &Path) -> Result<Arc<str>, SourceLoadError> {
(self)(path)
}
}
#[derive(Default)]
pub struct FileSystemResolver {
project_root: PathBuf,
}
impl FileSystemResolver {
pub fn new(project_root: PathBuf) -> Self {
Self { project_root }
}
}
impl SourceResolver for FileSystemResolver {
fn get_contents(&self, path: &Path) -> Result<Arc<str>, SourceLoadError> {
std::fs::read_to_string(path)
.map(Into::into)
.map_err(|cause| SourceLoadError::new(path.into(), cause))
}
fn resolve_raw_path(&self, path: &Path, included_from: Option<&Path>) -> PathBuf {
let path = Path::new(path);
let included_from = included_from.map(Path::new).and_then(Path::parent);
util::paths::resolve_path(path, &self.project_root, included_from)
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf, SourceLoadError> {
std::fs::canonicalize(path).map_err(|io_err| SourceLoadError::new(path.into(), io_err))
}
}
impl FileId {
pub(crate) const CURRENT_FILE: FileId = FileId(NonZeroU32::new(1).unwrap());
pub(crate) fn next() -> FileId {
use std::sync::atomic;
static COUNTER: atomic::AtomicU32 = atomic::AtomicU32::new(2);
FileId(NonZeroU32::new(COUNTER.fetch_add(1, atomic::Ordering::Relaxed)).unwrap())
}
}
impl Source {
pub(crate) fn new(path: PathBuf, contents: Arc<str>) -> Self {
let line_offsets = line_offsets(&contents);
Source {
path,
id: FileId::next(),
contents,
line_offsets,
}
}
pub fn text(&self) -> &str {
&self.contents
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn id(&self) -> FileId {
self.id
}
pub fn line_col_for_offset(&self, offset: usize) -> (usize, usize) {
let offset_idx = match self.line_offsets.binary_search(&offset) {
Ok(x) => x,
Err(x) => x - 1, };
let offset_of_line = self.line_offsets[offset_idx];
let offset_in_line = offset - offset_of_line;
(offset_idx + 1, offset_in_line)
}
pub fn line_containing_offset(&self, offset: usize) -> (usize, &str) {
let offset_idx = match self.line_offsets.binary_search(&offset) {
Ok(x) => x,
Err(x) => x - 1, };
let start_offset = self.line_offsets[offset_idx];
let end_offset = self
.line_offsets
.get(offset_idx + 1)
.copied()
.unwrap_or(self.contents.len());
(
offset_idx + 1,
self.contents[start_offset..end_offset].trim_end_matches('\n'),
)
}
pub fn offset_for_line_number(&self, line_number: usize) -> usize {
self.line_offsets[line_number - 1]
}
}
fn line_offsets(text: &str) -> Arc<[usize]> {
let mut result = vec![0];
result.extend(
text.bytes()
.enumerate()
.filter_map(|(i, b)| if b == b'\n' { Some(i + 1) } else { None }),
);
result.into()
}
impl SourceMap {
pub(crate) fn add_entry(&mut self, src: Range<usize>, dest: (FileId, usize)) {
if !src.is_empty() {
self.offsets.push((src, dest));
}
}
pub(crate) fn resolve_range(&self, global_range: Range<usize>) -> (FileId, Range<usize>) {
let (chunk, (file, local_offset)) = self
.offsets
.iter()
.find(|item| item.0.contains(&global_range.start))
.unwrap();
let chunk_offset = global_range.start - chunk.start;
let range_start = *local_offset + chunk_offset;
let len = global_range.end - global_range.start;
(*file, range_start..range_start + len)
}
}
impl SourceLoader {
pub(crate) fn new(resolver: Box<dyn SourceResolver>) -> Self {
Self {
sources: Default::default(),
resolver,
}
}
pub(crate) fn into_inner(self) -> Arc<SourceList> {
Arc::new(self.sources)
}
pub(crate) fn get(&self, id: &FileId) -> Option<&Source> {
self.sources.get(id)
}
pub(crate) fn source_for_path(
&mut self,
path: &Path,
included_by: Option<FileId>,
) -> Result<FileId, SourceLoadError> {
let included_by = included_by.map(|id| self.sources.get(&id).unwrap().path.as_path());
let path = self.resolver.resolve_raw_path(path.as_ref(), included_by);
let canonical = self.resolver.canonicalize(&path)?;
match self.sources.id_for_path(&canonical) {
Some(id) => Ok(id),
None => {
let source = self.resolver.resolve(&path)?;
let id = source.id;
self.sources.add(canonical, source);
Ok(id)
}
}
}
}
impl SourceList {
pub(crate) fn id_for_path(&self, path: impl AsRef<Path>) -> Option<FileId> {
self.ids.get(path.as_ref()).copied()
}
pub(crate) fn get(&self, id: &FileId) -> Option<&Source> {
self.sources.get(id)
}
fn add(&mut self, canonical_path: PathBuf, source: Source) {
self.ids.insert(canonical_path, source.id);
self.sources.insert(source.id, source);
}
pub(crate) fn format_diagnostic(&self, err: &Diagnostic, colorize: bool) -> String {
let mut s = String::new();
let source = self.get(&err.message.file).unwrap();
crate::util::highlighting::write_diagnostic(&mut s, err, source, None, colorize);
s
}
}
impl SourceLoadError {
pub fn new(path: PathBuf, cause: impl std::fmt::Display) -> Self {
Self {
cause: cause.to_string().into(),
path,
}
}
}