use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Span {
pub file: FileId,
pub start: u32,
pub end: u32,
}
impl Span {
pub fn new(file: FileId, start: u32, end: u32) -> Span {
Span { file, start, end }
}
pub fn to(self, other: Span) -> Span {
Span { file: self.file, start: self.start.min(other.start), end: self.end.max(other.end) }
}
pub fn dummy() -> Span {
Span { file: FileId(u32::MAX), start: 0, end: 0 }
}
pub fn is_dummy(&self) -> bool {
self.file.0 == u32::MAX
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineCol {
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone)]
pub struct SourceFile {
pub name: String,
pub base_name: String,
pub text: String,
line_starts: Vec<u32>,
}
impl SourceFile {
pub fn new(name: impl Into<String>, text: impl Into<String>) -> SourceFile {
let name: String = name.into();
let text: String = text.into();
let base_name = name.rsplit(['/', '\\']).next().unwrap_or(&name).to_string();
let mut line_starts = vec![0u32];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i as u32 + 1);
}
}
SourceFile { name, base_name, text, line_starts }
}
pub fn line_col(&self, offset: u32) -> LineCol {
let line = match self.line_starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i - 1,
};
let line_start = self.line_starts[line] as usize;
let col: usize =
self.text[line_start..(offset as usize).min(self.text.len())].chars().map(char::len_utf16).sum();
LineCol { line: line as u32 + 1, col: col as u32 + 1 }
}
pub fn slice(&self, span: Span) -> &str {
&self.text[span.start as usize..span.end as usize]
}
}
#[derive(Debug, Default, Clone)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
pub fn new() -> SourceMap {
SourceMap::default()
}
pub fn add(&mut self, name: impl Into<String>, text: impl Into<String>) -> FileId {
self.files.push(SourceFile::new(name, text));
FileId(self.files.len() as u32 - 1)
}
pub fn file(&self, id: FileId) -> &SourceFile {
&self.files[id.0 as usize]
}
pub fn files(&self) -> &[SourceFile] {
&self.files
}
pub fn text(&self, span: Span) -> &str {
self.file(span.file).slice(span)
}
pub fn location(&self, span: Span) -> Location {
if span.is_dummy() {
return Location { file: None, line: -1, col: -1 };
}
let f = self.file(span.file);
let lc = f.line_col(span.start);
Location { file: Some(f.name.clone()), line: lc.line as i64, col: lc.col as i64 }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
pub file: Option<String>,
pub line: i64,
pub col: i64,
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.file {
None => write!(f, "<no source>:{}:{}", self.line, self.col),
Some(name) => write!(f, "{}:{}:{}", name, self.line, self.col),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn columns_count_utf16_code_units() {
let f = SourceFile::new("t.p", "é😀x\ny");
assert_eq!(f.line_col(6), LineCol { line: 1, col: 4 });
assert_eq!(f.line_col(8), LineCol { line: 2, col: 1 });
}
}