use crate::Span;
pub type CodeId = usize;
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct Code {
pub source: Box<str>,
pub path: Option<Box<str>>,
}
#[derive(Debug, Clone, Copy)]
pub struct CodeRef<'a> {
input_files: &'a InputFiles,
pub file: CodeId,
}
impl<'a> CodeRef<'a> {
pub fn get_code(&self) -> &'a Code {
self.input_files.get_input(self.file)
}
pub fn get_offset(&self) -> usize {
self.input_files.get_input_offset(self.file)
}
pub fn get_span(&self) -> Span {
let input = &self.input_files.input_files[self.file];
Span::new(input.offset, input.code.source.len())
}
pub fn get_relative_span(&self, span: Span) -> Option<Span> {
let start = span.start().checked_sub(self.get_offset())?;
if start > self.input_files.input_files[self.file].code.source.len() {
None
} else {
Some(Span::new(start, span.len()))
}
}
}
#[derive(Debug)]
pub struct InputFile {
code: Code,
offset: usize,
}
#[derive(Debug, Default)]
pub struct InputFiles {
input_files: Vec<InputFile>,
}
impl InputFiles {
pub fn new() -> Self {
InputFiles::default()
}
pub fn add_input(&mut self, code: Code) -> CodeId {
let offset = self.get_total_offset();
let input_file = InputFile { code, offset };
self.input_files.push(input_file);
self.input_files.len() - 1
}
pub fn get_code_ref(&self, code_id: CodeId) -> CodeRef {
CodeRef {
file: code_id,
input_files: self,
}
}
pub fn find_by_filename(&self, filename: &str) -> Option<CodeId> {
self.input_files
.iter()
.enumerate()
.find(|(_, file)| {
file.code
.path
.as_ref()
.map_or(false, |fname| fname.as_ref() == filename)
})
.map(|(index, _)| index)
}
pub fn get_input(&self, code_id: CodeId) -> &Code {
&self.input_files[code_id].code
}
pub fn get_input_offset(&self, code_id: CodeId) -> usize {
self.input_files[code_id].offset
}
pub fn get_total_offset(&self) -> usize {
self.input_files
.last()
.map_or(0, |file| file.offset + file.code.source.len())
}
fn get_span_file(&self, span: Span) -> (usize, &InputFile) {
let index = match self
.input_files
.binary_search_by_key(&span.start(), |input_file| input_file.offset)
{
Ok(x) => x,
Err(x) => x - 1,
};
(index, &self.input_files[index])
}
pub fn get_span_code(&self, span: Span) -> CodeRef {
let (index, _) = self.get_span_file(span);
CodeRef {
file: index,
input_files: self,
}
}
pub fn get_span_str(&self, span: Span) -> &str {
let input_file = self.get_span_file(span).1;
let start = span.start() - input_file.offset;
&input_file.code.source[start..span.len() + start]
}
pub fn line_col(&self, span: Span) -> (usize, usize) {
let input_file = self.get_span_file(span).1;
let relative_start = span.start() - input_file.offset;
let (lines, line_start_idx) = input_file.code.source[..relative_start]
.char_indices()
.filter(|(_, chr)| *chr == '\n')
.fold((0_usize, 0), |(lines, _), (idx, _)| (lines + 1, idx + 1));
let col = input_file.code.source[line_start_idx..relative_start]
.chars()
.count();
(lines, col)
}
}
#[cfg(test)]
mod tests {
use crate::{Code, InputFiles, Span};
const FILE_1: &str = "Hello World!\nFoo";
const FILE_2: &str = "Bar\nBaz";
fn input_files() -> InputFiles {
let mut input_files = InputFiles::new();
input_files.add_input(Code {
path: None,
source: FILE_1.into(),
});
input_files.add_input(Code {
path: None,
source: FILE_2.into(),
});
input_files
}
#[test]
fn test_input_files_members() {
let input_files = input_files();
assert_eq!(input_files.get_input(0).source.as_ref(), FILE_1);
assert_eq!(input_files.get_input(1).source.as_ref(), FILE_2);
}
#[test]
fn test_input_files_offset() {
let input_files = input_files();
let offset_1 = input_files.get_input_offset(0);
let offset_2 = input_files.get_input_offset(1);
assert_eq!(offset_1, 0);
assert_eq!(offset_2, FILE_1.len());
}
#[test]
fn test_code_span() {
let input_files = input_files();
let code_1 = input_files.get_code_ref(0);
let code_2 = input_files.get_code_ref(1);
assert_eq!(code_1.get_span(), Span::new(0, FILE_1.len()));
assert_eq!(code_2.get_span(), Span::new(FILE_1.len(), FILE_2.len()));
}
#[test]
fn test_code_from_span() {
let input_files = input_files();
assert_eq!(input_files.get_span_file(Span::new(0, 1)).0, 0);
assert_eq!(
input_files.get_span_file(Span::new(FILE_1.len() - 1, 1)).0,
0
);
assert_eq!(input_files.get_span_file(Span::new(FILE_1.len(), 0)).0, 1);
assert_eq!(
input_files.get_span_file(Span::new(FILE_1.len() + 1, 1)).0,
1
);
}
#[test]
fn test_span_str() {
let input_files = input_files();
assert_eq!(input_files.get_span_str(Span::new(0, 0)), "");
assert_eq!(input_files.get_span_str(Span::new(0, 1)), "H");
assert_eq!(input_files.get_span_str(Span::new(0, 5)), "Hello");
assert_eq!(input_files.get_span_str(Span::new(6, 5)), "World");
assert_eq!(input_files.get_span_str(Span::new(13, 3)), "Foo");
assert_eq!(input_files.get_span_str(Span::new(16, 3)), "Bar");
assert_eq!(input_files.get_span_str(Span::new(20, 3)), "Baz");
}
#[test]
#[should_panic(expected = "byte index 18 is out of bounds")]
fn test_span_str_panic_a() {
input_files().get_span_str(Span::new(15, 3));
}
#[test]
#[should_panic(expected = "byte index 8 is out of bounds")]
fn test_span_str_panic_b() {
input_files().get_span_str(Span::new(16, 8));
}
#[test]
fn test_span_line() {
let input_files = input_files();
assert_eq!(input_files.line_col(Span::new(0, 0)), (0, 0));
assert_eq!(input_files.line_col(Span::new(0, 1)), (0, 0));
assert_eq!(input_files.line_col(Span::new(0, 5)), (0, 0));
assert_eq!(input_files.line_col(Span::new(6, 5)), (0, 6));
assert_eq!(input_files.line_col(Span::new(13, 3)), (1, 0));
assert_eq!(input_files.line_col(Span::new(16, 3)), (0, 0));
assert_eq!(input_files.line_col(Span::new(21, 2)), (1, 1));
}
}