use std::{
cmp::Ordering,
fmt::Debug,
iter::{Iterator, Peekable},
ops::Range,
path::{Path, PathBuf},
str::CharIndices,
sync::Arc,
};
use getset::{CopyGetters, Getters};
use super::{file_provider::FileProvider, Error};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Getters)]
pub struct SourceFile {
#[get = "pub"]
path: PathBuf,
#[get = "pub"]
identifier: String,
#[get = "pub"]
content: String,
lines: Vec<Range<usize>>,
}
#[allow(clippy::missing_fields_in_debug)]
impl Debug for SourceFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceFile")
.field("path", &self.path)
.field("lines", &self.lines)
.finish()
}
}
impl SourceFile {
fn new(path: PathBuf, identifier: String, content: String) -> Arc<Self> {
let lines = get_line_byte_positions(&content);
Arc::new(Self {
path,
identifier,
content,
lines,
})
}
#[must_use]
pub fn get_line(&self, line: usize) -> Option<&str> {
if line == 0 {
return None;
}
let line = line - 1;
self.lines
.get(line)
.map(|range| &self.content()[range.clone()])
}
#[must_use]
pub fn iter<'a>(self: &'a Arc<Self>) -> SourceIterator<'a> {
SourceIterator {
source_file: self,
iterator: self.content().char_indices().peekable(),
prev: None,
}
}
#[must_use]
pub fn line_amount(&self) -> usize {
self.lines.len()
}
pub fn load(
path: &Path,
identifier: String,
provider: &impl FileProvider,
) -> Result<Arc<Self>, Error> {
let source = provider.read_str(path)?;
Ok(Self::new(
path.to_path_buf(),
identifier,
source.into_owned(),
))
}
#[must_use]
pub fn get_location(&self, byte_index: usize) -> Option<Location> {
if self.content.is_char_boundary(byte_index) {
let line = self
.lines
.binary_search_by(|range| {
if range.contains(&byte_index) {
Ordering::Equal
} else if byte_index < range.start {
Ordering::Greater
} else {
Ordering::Less
}
})
.ok()?;
let line_starting_byte_index = self.lines[line].start;
let line_str = self.get_line(line + 1).unwrap();
let column = line_str
.char_indices()
.take_while(|(i, _)| *i + line_starting_byte_index < byte_index)
.count()
+ 1;
Some(Location {
line: line + 1,
column,
})
} else {
None
}
}
#[must_use]
pub fn path_relative(&self) -> Option<PathBuf> {
pathdiff::diff_paths(&self.path, std::env::current_dir().ok()?)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Getters, CopyGetters)]
pub struct Span {
#[get_copy = "pub"]
start: usize,
#[get_copy = "pub"]
end: usize,
#[get = "pub"]
source_file: Arc<SourceFile>,
}
#[allow(clippy::missing_fields_in_debug)]
impl Debug for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Span")
.field("start", &self.start)
.field("end", &self.end)
.field("content", &self.str())
.finish()
}
}
impl PartialEq for Span {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.source_file, &other.source_file)
&& self.start == other.start
&& self.end == other.end
}
}
impl Eq for Span {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for Span {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_ptr_value = Arc::as_ptr(&self.source_file) as usize;
let other_ptr_value = Arc::as_ptr(&other.source_file) as usize;
Some(self_ptr_value.cmp(&other_ptr_value).then_with(|| {
self.start
.cmp(&other.start)
.then_with(|| self.end.cmp(&other.end))
}))
}
}
impl Ord for Span {
fn cmp(&self, other: &Self) -> Ordering {
let self_ptr_value = Arc::as_ptr(&self.source_file) as usize;
let other_ptr_value = Arc::as_ptr(&other.source_file) as usize;
self_ptr_value
.cmp(&other_ptr_value)
.then_with(|| self.start.cmp(&other.start))
.then_with(|| self.end.cmp(&other.end))
}
}
impl std::hash::Hash for Span {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.start.hash(state);
self.end.hash(state);
Arc::as_ptr(&self.source_file).hash(state);
}
}
impl Span {
#[must_use]
pub fn new(source_file: Arc<SourceFile>, start: usize, end: usize) -> Option<Self> {
if start > end
|| !source_file.content().is_char_boundary(start)
|| source_file.content().len() < end
|| (source_file.content().len() + 1 != end
&& !source_file.content().is_char_boundary(end))
{
return None;
}
Some(Self {
start,
end,
source_file,
})
}
#[must_use]
pub fn to_end(source_file: Arc<SourceFile>, start: usize) -> Option<Self> {
if !source_file.content().is_char_boundary(start) {
return None;
}
Some(Self {
start,
end: source_file.content().len(),
source_file,
})
}
#[must_use]
pub fn str(&self) -> &str {
&self.source_file.content()[self.start..self.end]
}
#[must_use]
pub fn start_location(&self) -> Location {
self.source_file.get_location(self.start).unwrap()
}
#[must_use]
pub fn end_location(&self) -> Option<Location> {
self.source_file.get_location(self.end)
}
#[must_use]
pub fn join(&self, end: &Self) -> Option<Self> {
if !Arc::ptr_eq(&self.source_file, &end.source_file) || self.start > end.end {
return None;
}
Some(Self {
start: self.start,
end: end.end,
source_file: self.source_file.clone(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Location {
pub line: usize,
pub column: usize,
}
pub trait SourceElement {
fn span(&self) -> Span;
}
impl<T: SourceElement> SourceElement for Box<T> {
fn span(&self) -> Span {
self.as_ref().span()
}
}
#[derive(Debug, Clone, CopyGetters)]
pub struct SourceIterator<'a> {
#[get_copy = "pub"]
source_file: &'a Arc<SourceFile>,
iterator: Peekable<CharIndices<'a>>,
#[get_copy = "pub"]
prev: Option<(usize, char)>,
}
impl<'a> SourceIterator<'a> {
pub fn peek(&mut self) -> Option<(usize, char)> {
self.iterator.peek().copied()
}
}
impl<'a> Iterator for SourceIterator<'a> {
type Item = (usize, char);
fn next(&mut self) -> Option<Self::Item> {
let item = self.iterator.next();
if item.is_some() {
self.prev = item;
}
item
}
}
fn get_line_byte_positions(text: &str) -> Vec<Range<usize>> {
let mut current_position = 0;
let mut results = Vec::new();
let mut skip = false;
for (byte, char) in text.char_indices() {
if skip {
skip = false;
continue;
}
if char == '\n' {
#[allow(clippy::range_plus_one)]
results.push(current_position..byte + 1);
current_position = byte + 1;
}
if char == '\r' {
if text.as_bytes().get(byte + 1) == Some(&b'\n') {
results.push(current_position..byte + 2);
current_position = byte + 2;
skip = true;
} else {
#[allow(clippy::range_plus_one)]
results.push(current_position..byte + 1);
current_position = byte + 1;
}
}
}
results.push(current_position..text.len());
results
}