use crate::{BytePos, CharPos, pos::RelativeBytePos};
use std::{
fmt, io,
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MultiByteChar {
pub pos: RelativeBytePos,
pub bytes: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FileName {
Real(PathBuf),
Stdin,
Custom(String),
}
impl PartialEq<Path> for FileName {
fn eq(&self, other: &Path) -> bool {
match self {
Self::Real(p) => p == other,
_ => false,
}
}
}
impl PartialEq<&Path> for FileName {
fn eq(&self, other: &&Path) -> bool {
match self {
Self::Real(p) => p == *other,
_ => false,
}
}
}
impl PartialEq<PathBuf> for FileName {
fn eq(&self, other: &PathBuf) -> bool {
match self {
Self::Real(p) => p == other,
_ => false,
}
}
}
impl From<PathBuf> for FileName {
fn from(p: PathBuf) -> Self {
Self::Real(p)
}
}
impl From<&PathBuf> for FileName {
fn from(p: &PathBuf) -> Self {
Self::Real(p.clone())
}
}
impl From<&Path> for FileName {
fn from(p: &Path) -> Self {
Self::Real(p.to_path_buf())
}
}
impl From<String> for FileName {
fn from(s: String) -> Self {
Self::Custom(s)
}
}
impl From<&Self> for FileName {
fn from(s: &Self) -> Self {
s.clone()
}
}
impl FileName {
pub fn real(path: impl Into<PathBuf>) -> Self {
Self::Real(path.into())
}
pub fn custom(s: impl Into<String>) -> Self {
Self::Custom(s.into())
}
#[inline]
pub fn display(&self) -> FileNameDisplay<'_> {
let base_path =
crate::SessionGlobals::try_with(|g| g.and_then(|g| g.source_map.base_path()));
FileNameDisplay { inner: self, base_path }
}
#[inline]
pub fn as_real(&self) -> Option<&Path> {
match self {
Self::Real(path) => Some(path),
_ => None,
}
}
}
pub struct FileNameDisplay<'a> {
pub(crate) inner: &'a FileName,
pub(crate) base_path: Option<PathBuf>,
}
impl fmt::Display for FileNameDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner {
FileName::Real(path) => {
let path = if let Some(base_path) = &self.base_path
&& let Ok(rpath) = path.strip_prefix(base_path)
{
rpath
} else {
path.as_path()
};
path.display().fmt(f)
}
FileName::Stdin => f.write_str("<stdin>"),
FileName::Custom(s) => write!(f, "<{s}>"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct SourceFileId(u64);
impl SourceFileId {
pub(crate) fn new(filename: &FileName) -> Self {
use std::hash::{Hash, Hasher};
let mut hasher = solar_data_structures::map::FxHasher::with_seed(0);
filename.hash(&mut hasher);
Self(hasher.finish())
}
}
#[derive(Debug)]
pub struct OffsetOverflowError(pub(crate) ());
impl fmt::Display for OffsetOverflowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("files larger than 4GiB are not supported")
}
}
impl std::error::Error for OffsetOverflowError {}
impl From<OffsetOverflowError> for io::Error {
fn from(e: OffsetOverflowError) -> Self {
Self::new(io::ErrorKind::FileTooLarge, e)
}
}
#[derive(Clone, derive_more::Debug)]
#[non_exhaustive]
pub struct SourceFile {
pub name: FileName,
#[debug(skip)]
pub src: Arc<String>,
pub start_pos: BytePos,
pub source_len: RelativeBytePos,
#[debug(skip)]
pub lines: Vec<RelativeBytePos>,
#[debug(skip)]
pub multibyte_chars: Vec<MultiByteChar>,
}
impl PartialEq for SourceFile {
fn eq(&self, other: &Self) -> bool {
self.start_pos == other.start_pos
}
}
impl Eq for SourceFile {}
impl std::hash::Hash for SourceFile {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.start_pos.hash(state);
}
}
impl SourceFile {
pub(crate) fn new(
name: FileName,
id: SourceFileId,
mut src: String,
) -> Result<Self, OffsetOverflowError> {
debug_assert_eq!(id, SourceFileId::new(&name));
let source_len = src.len();
let source_len = u32::try_from(source_len).map_err(|_| OffsetOverflowError(()))?;
let (lines, multibyte_chars) = super::analyze::analyze_source_file(&src);
src.shrink_to_fit();
Ok(Self {
name,
src: Arc::new(src),
start_pos: BytePos::from_u32(0),
source_len: RelativeBytePos::from_u32(source_len),
lines,
multibyte_chars,
})
}
pub fn lines(&self) -> &[RelativeBytePos] {
&self.lines
}
pub fn count_lines(&self) -> usize {
self.lines().len()
}
#[inline]
pub fn absolute_position(&self, pos: RelativeBytePos) -> BytePos {
BytePos::from_u32(pos.to_u32() + self.start_pos.to_u32())
}
#[inline]
pub fn relative_position(&self, pos: BytePos) -> RelativeBytePos {
RelativeBytePos::from_u32(pos.to_u32() - self.start_pos.to_u32())
}
#[inline]
pub fn end_position(&self) -> BytePos {
self.absolute_position(self.source_len)
}
pub fn lookup_line(&self, pos: RelativeBytePos) -> Option<usize> {
self.lines().partition_point(|x| x <= &pos).checked_sub(1)
}
pub fn line_position(&self, line_number: usize) -> Option<usize> {
self.lines().get(line_number).map(|x| x.to_usize())
}
pub(crate) fn bytepos_to_file_charpos(&self, bpos: RelativeBytePos) -> CharPos {
let mut total_extra_bytes = 0;
for mbc in self.multibyte_chars.iter() {
if mbc.pos < bpos {
total_extra_bytes += mbc.bytes as u32 - 1;
assert!(bpos.to_u32() >= mbc.pos.to_u32() + mbc.bytes as u32);
} else {
break;
}
}
assert!(total_extra_bytes <= bpos.to_u32());
CharPos(bpos.to_usize() - total_extra_bytes as usize)
}
fn lookup_file_pos(&self, pos: RelativeBytePos) -> (usize, CharPos) {
let chpos = self.bytepos_to_file_charpos(pos);
match self.lookup_line(pos) {
Some(a) => {
let line = a + 1; let linebpos = self.lines()[a];
let linechpos = self.bytepos_to_file_charpos(linebpos);
let col = chpos - linechpos;
assert!(chpos >= linechpos);
(line, col)
}
None => (0, chpos),
}
}
pub fn lookup_file_pos_with_col_display(&self, pos: BytePos) -> (usize, CharPos, usize) {
let pos = self.relative_position(pos);
let (line, col_or_chpos) = self.lookup_file_pos(pos);
if line > 0 {
let Some(code) = self.get_line(line - 1) else {
debug!("couldn't find line {line} in {:?}", self.name);
return (line, col_or_chpos, col_or_chpos.0);
};
let display_col = code.chars().take(col_or_chpos.0).map(char_width).sum();
(line, col_or_chpos, display_col)
} else {
(0, col_or_chpos, col_or_chpos.0)
}
}
pub fn get_line(&self, line_number: usize) -> Option<&str> {
fn get_until_newline(src: &str, begin: usize) -> &str {
let slice = &src[begin..];
match slice.find('\n') {
Some(e) => &slice[..e],
None => slice,
}
}
let start = self.lines().get(line_number)?.to_usize();
Some(get_until_newline(&self.src, start))
}
pub fn get_lines(&self, range: RangeInclusive<usize>) -> Option<&str> {
fn get_until_newline(src: &str, start: usize, end: usize) -> &str {
match src[end..].find('\n') {
Some(e) => &src[start..end + e + 1],
None => &src[start..],
}
}
let (start, end) = range.into_inner();
let lines = self.lines();
let start = lines.get(start)?.to_usize();
let end = lines.get(end)?.to_usize();
Some(get_until_newline(&self.src, start, end))
}
#[inline]
pub fn contains(&self, byte_pos: BytePos) -> bool {
byte_pos >= self.start_pos && byte_pos <= self.end_position()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.source_len.to_u32() == 0
}
pub fn original_relative_byte_pos(&self, pos: BytePos) -> RelativeBytePos {
let pos = self.relative_position(pos);
RelativeBytePos::from_u32(pos.0)
}
}
pub fn char_width(ch: char) -> usize {
match ch {
'\t' => 4,
_ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
}
}