use crate::ByteSpan;
use crate::SourceSpan;
use crate::SourcePosition;
use std::path::Path;
use std::path::PathBuf;
#[derive(Clone)]
enum SourceMapData<'src> {
SourceText {
source: &'src str,
line_starts: Vec<u32>,
},
PrecomputedColumns {
entries: Vec<(u32, SourcePosition)>,
},
}
impl<'src> SourceMapData<'src> {
fn resolve_offset(
&self,
byte_offset: u32,
) -> Option<SourcePosition> {
match self {
Self::SourceText {
source,
line_starts,
} => {
let offset = byte_offset as usize;
if offset > source.len() {
return None;
}
let line_index =
line_starts.partition_point(|&ls| ls <= byte_offset);
let line =
if line_index > 0 { line_index - 1 } else { 0 };
let line_start = line_starts[line] as usize;
if !source.is_char_boundary(offset) {
return None;
}
let line_slice = &source[line_start..offset];
let (col_utf8, col_utf16) = if line_slice.is_ascii() {
let len = line_slice.len();
(len, len)
} else {
let mut utf8: usize = 0;
let mut utf16: usize = 0;
for ch in line_slice.chars() {
utf8 += 1;
utf16 += ch.len_utf16();
}
(utf8, utf16)
};
Some(SourcePosition::new(
line,
col_utf8,
Some(col_utf16),
offset,
))
},
Self::PrecomputedColumns { entries } => {
if entries.is_empty() {
return None;
}
let idx = entries
.partition_point(|&(off, _)| off <= byte_offset);
if idx > 0 {
Some(entries[idx - 1].1)
} else {
None
}
},
}
}
fn source(&self) -> Option<&'src str> {
match self {
Self::SourceText { source, .. } => Some(source),
Self::PrecomputedColumns { .. } => None,
}
}
}
#[derive(Clone)]
pub struct SourceMap<'src> {
data: SourceMapData<'src>,
file_path: Option<PathBuf>,
}
impl<'src> std::fmt::Debug for SourceMap<'src> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match &self.data {
SourceMapData::SourceText { line_starts, .. } => {
format!("SourceText({} lines)", line_starts.len())
},
SourceMapData::PrecomputedColumns { entries } => {
format!("PrecomputedColumns({} entries)", entries.len())
},
};
f.debug_struct("SourceMap")
.field("mode", &mode)
.field("file_path", &self.file_path)
.finish()
}
}
impl<'src> SourceMap<'src> {
pub fn new_with_source(
source: &'src str,
file_path: Option<PathBuf>,
) -> Self {
let line_starts = Self::compute_line_starts(source);
Self {
data: SourceMapData::SourceText {
source,
line_starts,
},
file_path,
}
}
pub fn new_precomputed(
entries: Vec<(u32, SourcePosition)>,
file_path: Option<PathBuf>,
) -> Self {
debug_assert!(
entries.windows(2).all(|w| w[0].0 <= w[1].0),
"new_precomputed entries must be sorted by byte offset",
);
Self {
data: SourceMapData::PrecomputedColumns { entries },
file_path,
}
}
pub fn empty() -> Self {
Self {
data: SourceMapData::PrecomputedColumns {
entries: Vec::new(),
},
file_path: None,
}
}
pub fn source(&self) -> Option<&'src str> {
self.data.source()
}
pub fn file_path(&self) -> Option<&Path> {
self.file_path.as_deref()
}
pub fn resolve_offset(
&self,
byte_offset: u32,
) -> Option<SourcePosition> {
self.data.resolve_offset(byte_offset)
}
pub fn resolve_span(
&self,
span: ByteSpan,
) -> Option<SourceSpan> {
let start = self.data.resolve_offset(span.start)?;
let end = self.data.resolve_offset(span.end)?;
Some(match &self.file_path {
Some(path) => {
SourceSpan::with_file(start, end, path.clone())
},
None => SourceSpan::new(start, end),
})
}
pub fn get_line(&self, line_index: usize) -> Option<&'src str> {
match &self.data {
SourceMapData::SourceText { source, line_starts } => {
if line_index >= line_starts.len() {
return None;
}
let start = line_starts[line_index] as usize;
let end = if line_index + 1 < line_starts.len() {
line_starts[line_index + 1] as usize
} else {
source.len()
};
let line = &source[start..end];
let line = line.strip_suffix("\r\n")
.or_else(|| line.strip_suffix('\n'))
.or_else(|| line.strip_suffix('\r'))
.unwrap_or(line);
Some(line)
},
SourceMapData::PrecomputedColumns { .. } => None,
}
}
pub fn line_count(&self) -> usize {
match &self.data {
SourceMapData::SourceText { line_starts, .. } => {
line_starts.len()
},
SourceMapData::PrecomputedColumns { entries } => {
entries.last()
.map(|(_, pos)| pos.line() + 1)
.unwrap_or(0)
},
}
}
fn compute_line_starts(source: &str) -> Vec<u32> {
let bytes = source.as_bytes();
let len = bytes.len();
let mut line_starts = Vec::with_capacity(1 + len / 40);
line_starts.push(0);
let mut i = 0;
while let Some(offset) = memchr::memchr2(b'\n', b'\r', &bytes[i..]) {
i += offset;
match bytes[i] {
b'\n' => {
line_starts.push((i + 1) as u32);
i += 1;
},
b'\r' => {
if i + 1 < len && bytes[i + 1] == b'\n' {
line_starts.push((i + 2) as u32);
i += 2;
} else {
line_starts.push((i + 1) as u32);
i += 1;
}
},
_ => unreachable!(),
}
}
line_starts
}
}