use crate::{SourceId, SourceSpan};
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub struct SourceMap<'a> {
source: Option<SourceId>,
name: Option<Cow<'a, str>>,
input: &'a str,
line_starts: Vec<usize>,
}
impl<'a> SourceMap<'a> {
#[must_use]
pub fn anonymous(input: &'a str) -> Self {
Self::new(None, None, input)
}
#[must_use]
pub fn new(source: Option<SourceId>, name: Option<Cow<'a, str>>, input: &'a str) -> Self {
let mut line_starts = Vec::new();
line_starts.push(0);
for (index, byte) in input.bytes().enumerate() {
if byte == b'\n' {
line_starts.push(index + 1);
}
}
Self {
source,
name,
input,
line_starts,
}
}
#[must_use]
pub const fn source_id(&self) -> Option<SourceId> {
self.source
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub const fn len(&self) -> usize {
self.input.len()
}
#[must_use]
pub const fn input(&self) -> &'a str {
self.input
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.input.is_empty()
}
#[must_use]
pub fn line_column(&self, byte: usize) -> (usize, usize) {
let byte = byte.min(self.input.len());
let line_index = match self.line_starts.binary_search(&byte) {
Ok(index) => index,
Err(0) => 0,
Err(index) => index - 1,
};
let line_start = self.line_starts[line_index];
let column = self.input[line_start..byte].chars().count() + 1;
(line_index + 1, column)
}
#[must_use]
pub fn byte_at_line_column(&self, line: usize, column: usize) -> Option<usize> {
if line == 0 || column == 0 {
return None;
}
let line_start = *self.line_starts.get(line - 1)?;
let line_end = self
.line_starts
.get(line)
.map_or(self.input.len(), |next| next.saturating_sub(1));
let line_text = self.input.get(line_start..line_end)?;
if column == 1 {
return Some(line_start);
}
let mut current_column = 1usize;
for (offset, _) in line_text.char_indices() {
if current_column == column {
return Some(line_start + offset);
}
current_column += 1;
}
if current_column == column {
Some(line_end)
} else {
None
}
}
#[must_use]
pub fn span(&self, byte_start: usize, byte_end: usize) -> SourceSpan {
let byte_start = byte_start.min(self.input.len());
let byte_end = byte_end.min(self.input.len()).max(byte_start);
let (line, column) = self.line_column(byte_start);
let (end_line, end_column) = self.line_column(byte_end);
let span = SourceSpan::with_end(byte_start, byte_end, line, column, end_line, end_column);
self.source.map_or(span, |source| span.with_source(source))
}
#[must_use]
pub fn slice(&self, span: SourceSpan) -> Option<&'a str> {
if span.source.is_some() && span.source != self.source {
return None;
}
self.input.get(span.byte_start..span.byte_end)
}
#[must_use]
pub fn snippet(&self, span: SourceSpan, max_chars: usize) -> Option<String> {
if span.source.is_some() && span.source != self.source {
return None;
}
let anchor_start = if span.is_empty() && span.byte_start > 0 {
span.byte_start - 1
} else {
span.byte_start
};
let anchor_end = if span.is_empty() && span.byte_end > 0 {
span.byte_end - 1
} else {
span.byte_end
};
let start = self.input[..anchor_start]
.rfind('\n')
.map_or(0, |index| index + 1);
let end = self.input[anchor_end..]
.find('\n')
.map_or(self.input.len(), |index| anchor_end + index);
let snippet = self.input.get(start..end)?;
if snippet.chars().count() <= max_chars {
return Some(snippet.to_string());
}
Some(snippet.chars().take(max_chars).collect())
}
}