use std::ops::Range;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(u32);
impl FileId {
pub const ZERO: FileId = FileId(0);
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, 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: end.max(start),
}
}
pub fn range(self) -> Range<usize> {
self.start as usize..self.end as usize
}
pub fn len(self) -> u32 {
self.end - self.start
}
pub fn is_empty(self) -> bool {
self.start == self.end
}
pub fn to(self, other: Span) -> Span {
if self.file != other.file {
return self;
}
Span::new(
self.file,
self.start.min(other.start),
self.end.max(other.end),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LineCol {
pub line: u32,
pub column: u32,
}
pub const MAX_FILE_SIZE: usize = u32::MAX as usize;
#[derive(Debug)]
pub struct SourceFile {
name: String,
text: String,
line_starts: Vec<u32>,
}
impl SourceFile {
fn new(name: String, text: String) -> SourceFile {
let mut line_starts = vec![0];
for (offset, byte) in text.bytes().enumerate() {
if byte == b'\n' {
line_starts.push(to_u32(offset + 1));
}
}
SourceFile {
name,
text,
line_starts,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn text(&self) -> &str {
&self.text
}
pub fn line_index(&self, offset: u32) -> usize {
match self.line_starts.binary_search(&offset) {
Ok(line) => line,
Err(next) => next.saturating_sub(1),
}
}
pub fn line_range(&self, index: usize) -> Option<Range<usize>> {
let start = *self.line_starts.get(index)? as usize;
let end = match self.line_starts.get(index + 1) {
Some(next) => *next as usize,
None => self.text.len(),
};
Some(start..end)
}
pub fn line_col(&self, offset: u32) -> LineCol {
let line = self.line_index(offset);
let line_start = self.line_starts.get(line).copied().unwrap_or(0) as usize;
let offset = (offset as usize).min(self.text.len()).max(line_start);
let prefix = self.text.get(line_start..offset).unwrap_or("");
LineCol {
line: to_u32(line + 1),
column: to_u32(prefix.chars().count() + 1),
}
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
pub fn new() -> SourceMap {
SourceMap::default()
}
pub fn add(&mut self, name: impl Into<String>, mut text: String) -> FileId {
if text.len() > MAX_FILE_SIZE {
let mut cut = MAX_FILE_SIZE;
while !text.is_char_boundary(cut) {
cut -= 1;
}
text.truncate(cut);
}
let id = FileId(to_u32(self.files.len()));
self.files.push(SourceFile::new(name.into(), text));
id
}
pub fn get(&self, id: FileId) -> Option<&SourceFile> {
self.files.get(id.index())
}
pub fn iter(&self) -> impl Iterator<Item = (FileId, &SourceFile)> {
self.files
.iter()
.enumerate()
.map(|(index, file)| (FileId(to_u32(index)), file))
}
}
fn to_u32(value: usize) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
fn file(text: &str) -> SourceFile {
SourceFile::new("test.wolf".to_string(), text.to_string())
}
#[test]
fn line_col_counts_scalar_values() {
let f = file("ab\nпривет x\n");
assert_eq!(f.line_col(0), LineCol { line: 1, column: 1 });
assert_eq!(f.line_col(3), LineCol { line: 2, column: 1 });
assert_eq!(f.line_col(3 + 13), LineCol { line: 2, column: 8 });
}
#[test]
fn line_col_at_end_of_file() {
let f = file("a\nb");
assert_eq!(f.line_col(3), LineCol { line: 2, column: 2 });
assert_eq!(f.line_col(100), LineCol { line: 2, column: 2 });
}
#[test]
fn line_col_inside_a_character_is_clamped() {
let f = file("я");
assert_eq!(f.line_col(1).line, 1);
}
#[test]
fn line_ranges_include_terminators() {
let f = file("a\r\nbc\n");
assert_eq!(f.line_range(0), Some(0..3));
assert_eq!(f.line_range(1), Some(3..6));
assert_eq!(f.line_range(2), Some(6..6));
assert_eq!(f.line_range(3), None);
}
#[test]
fn span_to_merges() {
let mut map = SourceMap::new();
let id = map.add("a", "hello".to_string());
let a = Span::new(id, 1, 2);
let b = Span::new(id, 3, 5);
assert_eq!(a.to(b), Span::new(id, 1, 5));
assert_eq!(Span::new(id, 4, 2).len(), 0);
}
}