use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, OnceLock, RwLock},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LineCol {
pub line: u32,
pub column: u32,
}
#[derive(Clone, Debug)]
pub struct LineIndex {
line_starts: Vec<u32>,
}
impl LineIndex {
#[must_use]
pub fn build(src: &str) -> Self {
let mut starts = Vec::with_capacity(src.len() / 32 + 4);
starts.push(0);
for (i, b) in src.bytes().enumerate() {
if b == b'\n' {
let next = i.saturating_add(1);
starts.push(u32::try_from(next).unwrap_or(u32::MAX));
}
}
Self {
line_starts: starts,
}
}
#[must_use]
pub fn locate(&self, byte: u32) -> LineCol {
let line_idx = self
.line_starts
.partition_point(|&start| start <= byte)
.saturating_sub(1);
let line_start = self.line_starts.get(line_idx).copied().unwrap_or_default();
LineCol {
line: u32::try_from(line_idx + 1).unwrap_or(u32::MAX),
column: byte.saturating_sub(line_start).saturating_add(1),
}
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
inner: RwLock<HashMap<PathBuf, Arc<SourceEntry>>>,
}
#[derive(Debug)]
struct SourceEntry {
src: Arc<str>,
line_index: OnceLock<LineIndex>,
}
impl SourceMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get(&self, path: &Path) -> Option<Arc<str>> {
let read = self.inner.read().ok()?;
read.get(path).map(|entry| Arc::clone(&entry.src))
}
pub fn insert(&self, path: &Path, src: Arc<str>) -> Arc<str> {
let entry = Arc::new(SourceEntry {
src: Arc::clone(&src),
line_index: OnceLock::new(),
});
if let Ok(mut write) = self.inner.write() {
write.insert(path.to_path_buf(), entry);
}
src
}
#[must_use]
pub fn line_index(&self, path: &Path) -> Option<LineIndex> {
let read = self.inner.read().ok()?;
let entry = read.get(path)?;
let li = entry
.line_index
.get_or_init(|| LineIndex::build(&entry.src))
.clone();
Some(li)
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.read().map_or(0, |m| m.len())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
#[test]
fn test_line_index_locates_first_byte_at_1_1() {
let li = LineIndex::build("");
assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
}
#[test]
fn test_line_index_locates_across_newlines() {
let src = "abc\ndefgh\ni";
let li = LineIndex::build(src);
assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
assert_eq!(li.locate(2), LineCol { line: 1, column: 3 });
assert_eq!(li.locate(3), LineCol { line: 1, column: 4 }); assert_eq!(li.locate(4), LineCol { line: 2, column: 1 });
assert_eq!(li.locate(8), LineCol { line: 2, column: 5 });
assert_eq!(li.locate(10), LineCol { line: 3, column: 1 });
}
#[test]
fn test_line_index_handles_trailing_newline() {
let src = "x\n";
let li = LineIndex::build(src);
assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
assert_eq!(li.locate(2), LineCol { line: 2, column: 1 });
}
#[test]
fn test_line_index_clamps_byte_past_end() {
let li = LineIndex::build("abc");
let pos = li.locate(1_000);
assert_eq!(pos.line, 1);
assert!(pos.column >= 1);
}
#[test]
fn test_source_map_round_trip() {
let map = SourceMap::new();
let path = PathBuf::from("/tmp/x.tf");
let src: Arc<str> = Arc::from("a\nb\nc");
let returned = map.insert(&path, Arc::clone(&src));
assert_eq!(&*returned, "a\nb\nc");
let cached = map.get(&path).unwrap();
assert!(Arc::ptr_eq(&cached, &src));
assert_eq!(map.len(), 1);
let li = map.line_index(&path).unwrap();
assert_eq!(li.locate(2), LineCol { line: 2, column: 1 });
}
#[test]
fn test_source_map_returns_none_for_uncached() {
let map = SourceMap::new();
assert!(map.get(Path::new("/nope")).is_none());
assert!(map.line_index(Path::new("/nope")).is_none());
}
#[test]
fn test_source_map_overwrite_replaces_entry() {
let map = SourceMap::new();
let path = PathBuf::from("/tmp/x.tf");
map.insert(&path, Arc::from("v1"));
map.insert(&path, Arc::from("v2"));
assert_eq!(&*map.get(&path).unwrap(), "v2");
}
}