use rustledger_parser::Span;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SourceFile {
pub id: usize,
pub path: PathBuf,
pub source: Arc<str>,
line_starts: Vec<usize>,
}
impl SourceFile {
fn new(id: usize, path: PathBuf, source: Arc<str>) -> Self {
let line_starts = std::iter::once(0)
.chain(source.match_indices('\n').map(|(i, _)| i + 1))
.collect();
Self {
id,
path,
source,
line_starts,
}
}
#[must_use]
pub fn line_col(&self, offset: usize) -> (usize, usize) {
let line = self
.line_starts
.iter()
.rposition(|&start| start <= offset)
.unwrap_or(0);
let col = offset - self.line_starts[line];
(line + 1, col + 1)
}
#[must_use]
pub fn span_text(&self, span: &Span) -> &str {
&self.source[span.start..span.end.min(self.source.len())]
}
#[must_use]
pub fn line(&self, line_num: usize) -> Option<&str> {
if line_num == 0 || line_num > self.line_starts.len() {
return None;
}
let start = self.line_starts[line_num - 1];
let end = if line_num < self.line_starts.len() {
self.line_starts[line_num] - 1 } else {
self.source.len()
};
Some(&self.source[start..end])
}
#[must_use]
pub const fn num_lines(&self) -> usize {
self.line_starts.len()
}
#[must_use]
pub fn line_start(&self, line_num: usize) -> Option<usize> {
if line_num == 0 || line_num > self.line_starts.len() {
return None;
}
Some(self.line_starts[line_num - 1])
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_file(&mut self, path: PathBuf, source: Arc<str>) -> usize {
let id = self.files.len();
self.files.push(SourceFile::new(id, path, source));
id
}
#[must_use]
pub fn get(&self, id: usize) -> Option<&SourceFile> {
self.files.get(id)
}
#[must_use]
pub fn get_by_path(&self, path: &std::path::Path) -> Option<&SourceFile> {
self.files.iter().find(|f| f.path == path)
}
#[must_use]
pub fn files(&self) -> &[SourceFile] {
&self.files
}
#[must_use]
pub fn format_span(&self, file_id: usize, span: &Span) -> String {
if let Some(file) = self.get(file_id) {
let (line, col) = file.line_col(span.start);
format!("{}:{}:{}", file.path.display(), line, col)
} else {
format!("?:{}..{}", span.start, span.end)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_col() {
let source: Arc<str> = "line 1\nline 2\nline 3".into();
let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
assert_eq!(file.line_col(0), (1, 1)); assert_eq!(file.line_col(5), (1, 6)); assert_eq!(file.line_col(7), (2, 1)); assert_eq!(file.line_col(14), (3, 1)); }
#[test]
fn test_get_line() {
let source: Arc<str> = "line 1\nline 2\nline 3".into();
let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
assert_eq!(file.line(1), Some("line 1"));
assert_eq!(file.line(2), Some("line 2"));
assert_eq!(file.line(3), Some("line 3"));
assert_eq!(file.line(0), None);
assert_eq!(file.line(4), None);
}
#[test]
fn test_line_start() {
let source: Arc<str> = "line 1\nline 2\nline 3".into();
let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
assert_eq!(file.line_start(1), Some(0)); assert_eq!(file.line_start(2), Some(7)); assert_eq!(file.line_start(3), Some(14));
assert_eq!(file.line_start(0), None); assert_eq!(file.line_start(4), None); assert_eq!(file.line_start(100), None); }
#[test]
fn test_source_map() {
let mut sm = SourceMap::new();
let id = sm.add_file(PathBuf::from("test.beancount"), "content".into());
assert_eq!(id, 0);
assert!(sm.get(0).is_some());
assert!(sm.get(1).is_none());
}
}