use std::path::Path;
use std::{fmt, fs, io};
#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SourceFile {
pub contents: String,
file_names: Vec<String>,
file_lines: Vec<usize>,
line_lengths: Vec<usize>,
}
impl SourceFile {
pub fn new() -> Self {
Default::default()
}
pub fn add_file(&mut self, filename: impl AsRef<Path>) -> io::Result<()> {
let filename = filename.as_ref();
let file = fs::read_to_string(filename)?;
self.add_file_raw(filename.display(), file);
Ok(())
}
pub fn add_file_raw(&mut self, name: impl fmt::Display, contents: impl Into<String>) {
let contents = contents.into();
if contents.is_empty() {
return;
}
let mut num_lines = 0;
let mut lines = contents.split('\n').peekable();
while let Some(line) = lines.next() {
if lines.peek().is_some() {
num_lines += 1;
self.line_lengths.push(line.len() + 1);
} else if line.is_empty() {
} else {
num_lines += 1;
self.line_lengths.push(line.len());
}
}
self.file_names.push(name.to_string());
self.file_lines.push(num_lines);
self.contents += &contents;
}
pub fn resolve_offset<'a>(&'a self, offset: usize) -> Option<Position<'a>> {
let mut line_acc = *self.line_lengths.get(0)?;
let mut line_idx = 0;
while line_acc <= offset {
line_idx += 1;
line_acc += *self.line_lengths.get(line_idx)?;
}
line_acc -= self.line_lengths[line_idx];
let mut file_acc = self.file_lines[0];
let mut file_idx = 0;
while file_acc <= line_idx {
file_idx += 1;
file_acc += self.file_lines[file_idx];
}
file_acc -= self.file_lines[file_idx];
Some(Position::new(
&self.file_names[file_idx],
line_idx - file_acc,
offset - line_acc,
))
}
pub fn resolve_offset_span<'a>(&'a self, start: usize, end: usize) -> Option<Span<'a>> {
if end < start {
return None;
}
Some(Span {
start: self.resolve_offset(start)?,
end: self.resolve_offset(end)?,
})
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Position<'a> {
pub filename: &'a str,
pub line: usize,
pub col: usize,
}
impl<'a> Position<'a> {
fn new(filename: &'a str, line: usize, col: usize) -> Position<'a> {
Position {
filename: filename.as_ref(),
line,
col,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Span<'a> {
pub start: Position<'a>,
pub end: Position<'a>,
}
#[cfg(test)]
mod tests {
extern crate tempfile;
use self::tempfile::NamedTempFile;
use super::{Position, SourceFile, Span};
use std::io::Write;
#[test]
fn empty() {
let sourcefile = SourceFile::default();
assert!(sourcefile.resolve_offset(0).is_none());
}
#[test]
fn smoke() {
test_files(
&[
"A file with\ntwo lines.\n",
"Another file with\ntwo more lines.\n",
],
&[
(0, (0, 0, 0)), (5, (0, 0, 5)), (11, (0, 0, 11)), (12, (0, 1, 0)), (13, (0, 1, 1)),
(13, (0, 1, 1)),
(22, (0, 1, 10)),
(23, (1, 0, 0)),
(24, (1, 0, 1)),
(40, (1, 0, 17)),
(41, (1, 1, 0)),
(42, (1, 1, 1)),
(56, (1, 1, 15)),
],
&[((0, 5), (0, 0, 0), (0, 0, 5))],
)
}
fn test_files<'a>(
files: &[impl AsRef<str>],
offset_tests: &[(usize, (usize, usize, usize))],
offset_span_tests: &[((usize, usize), (usize, usize, usize), (usize, usize, usize))],
) {
let mut sourcefile = SourceFile::default();
let mut file_handles = Vec::new(); for contents in files {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", contents.as_ref()).unwrap();
sourcefile.add_file(file.path()).unwrap();
file_handles.push(file);
}
for &(offset, (file_idx, line, col)) in offset_tests {
let filename = format!("{}", file_handles[file_idx].path().display());
let pos = sourcefile.resolve_offset(offset);
assert_eq!(pos.unwrap(), Position::new(&filename, line, col));
}
for &(
(start, end),
(file_idx_start, line_start, col_start),
(file_idx_end, line_end, col_end),
) in offset_span_tests
{
let start_filename = format!("{}", file_handles[file_idx_start].path().display());
let end_filename = format!("{}", file_handles[file_idx_end].path().display());
assert_eq!(
sourcefile.resolve_offset_span(start, end).unwrap(),
Span {
start: Position::new(&start_filename, line_start, col_start),
end: Position::new(&end_filename, line_end, col_end),
}
);
}
}
#[test]
fn test_raw() {
let mut sourcefile = SourceFile::new();
sourcefile.add_file_raw("test", " ");
assert_eq!(*sourcefile.line_lengths.last().unwrap(), 1);
}
}