use std::{borrow::Cow, fmt, ops::Range, path::Path};
use bstr::ByteSlice;
use logix_vfs::LogixVfs;
use crate::{loader::CachedFile, LogixLoader};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SourceSpan {
file: CachedFile,
pos: usize,
line: usize,
col: Range<u16>,
}
impl SourceSpan {
pub fn empty() -> Self {
Self {
file: CachedFile::empty(),
pos: 0,
line: 0,
col: 0..0,
}
}
pub fn new_for_test(
loader: &LogixLoader<impl LogixVfs>,
path: impl AsRef<Path>,
line: usize,
col: usize,
len: usize,
) -> Self {
let file = loader.get_file(path).unwrap();
let mut pos = 0;
for (i, line_data) in file.data().lines_with_terminator().enumerate() {
if i + 1 == line {
pos += col;
break;
}
pos += line_data.len();
}
Self::new(&file, pos, line, col, len)
}
pub(crate) fn new(file: &CachedFile, pos: usize, line: usize, col: usize, len: usize) -> Self {
let scol = u16::try_from(col).unwrap();
let ecol = u16::try_from(col + len).unwrap();
Self {
file: file.clone(),
pos,
line,
col: scol..ecol,
}
}
pub fn path(&self) -> &Path {
self.file.path()
}
pub fn line(&self) -> usize {
self.line
}
pub fn col(&self) -> usize {
self.col.start.into()
}
pub fn value(&self) -> Cow<str> {
String::from_utf8_lossy(&self.file.data()[self.pos..self.pos + self.col.len()])
}
pub fn lines(
&self,
context: usize,
) -> impl Iterator<Item = (usize, Option<Range<usize>>, Cow<str>)> {
self.file
.lines()
.enumerate()
.skip(self.line.saturating_sub(context + 1))
.map_while(move |(i, line)| {
let ln = i + 1;
if ln == self.line {
Some((
ln,
Some(usize::from(self.col.start)..usize::from(self.col.end)),
line.to_str_lossy(),
))
} else if ln <= self.line + context {
Some((ln, None, line.to_str_lossy()))
} else {
None
}
})
}
pub fn with_off(&self, off: usize, len: usize) -> Self {
let off = u16::try_from(off).unwrap();
let len = u16::try_from(len).unwrap();
Self {
file: self.file.clone(),
pos: self.pos + usize::from(off),
line: self.line,
col: self.col.start + off..self.col.start + off + len,
}
}
pub fn calc_ln_width(&self, extra: usize) -> usize {
match self.line + extra {
0..=999 => 3,
1000..=9999 => 4,
10000..=99999 => 5,
100000..=999999 => 6,
_ => 10,
}
}
pub(crate) fn join(&self, other: &Self) -> SourceSpan {
assert_eq!(self.file, other.file);
let mut ret = self.clone();
if ret.line == other.line {
ret.pos = other.pos.min(ret.pos);
ret.col.start = other.col.start.min(ret.col.start);
ret.col.end = other.col.end.max(ret.col.end);
} else {
todo!()
}
ret
}
pub(crate) fn from_pos(file: &CachedFile, pos: usize) -> SourceSpan {
let mut cur = 0;
let mut ln = 1;
let mut col = 0..0;
for line in file.data().lines_with_terminator() {
let range = cur..cur + line.len();
if range.contains(&pos) {
let start = u16::try_from(pos - range.start).unwrap();
col = start..start + 1;
break;
}
cur = range.end;
ln += 1;
}
Self {
file: file.clone(),
pos,
line: ln,
col,
}
}
}
impl fmt::Display for SourceSpan {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}:{}:{}",
self.file.path().display(),
self.line,
self.col.start
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coverage_hacks() {
assert_eq!(
SourceSpan {
file: CachedFile::from_slice("test.logix", b"hello world"),
pos: 6,
line: 1,
col: 6..11,
}
.value(),
"world"
);
assert_eq!(SourceSpan::empty().value(), "");
}
#[test]
fn line_width() {
assert_eq!(SourceSpan::empty().calc_ln_width(0), 3);
assert_eq!(SourceSpan::empty().calc_ln_width(10), 3);
assert_eq!(SourceSpan::empty().calc_ln_width(100), 3);
assert_eq!(SourceSpan::empty().calc_ln_width(1000), 4);
assert_eq!(SourceSpan::empty().calc_ln_width(10000), 5);
assert_eq!(SourceSpan::empty().calc_ln_width(100000), 6);
assert_eq!(SourceSpan::empty().calc_ln_width(1000000), 10);
assert_eq!(SourceSpan::empty().calc_ln_width(10000000), 10);
assert_eq!(SourceSpan::empty().calc_ln_width(100000000), 10);
assert_eq!(SourceSpan::empty().calc_ln_width(1000000000), 10);
assert_eq!(SourceSpan::empty().calc_ln_width(10000000000), 10);
}
}