use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use rustc_hash::FxHashMap;
use sui_intern::Symbol;
#[derive(Debug, Default)]
pub struct AttrPositions {
pub file: Option<PathBuf>,
pub keys: FxHashMap<Symbol, u32>,
}
impl AttrPositions {
#[must_use]
pub fn new(file: Option<PathBuf>) -> Self {
Self {
file,
keys: FxHashMap::default(),
}
}
pub fn insert(&mut self, key: Symbol, offset: u32) {
self.keys.insert(key, offset);
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
}
thread_local! {
static SOURCE_TEXTS: RefCell<FxHashMap<PathBuf, Rc<str>>> =
RefCell::new(FxHashMap::default());
}
pub fn register_source(file: Option<&Path>, text: &str) {
let Some(file) = file else { return };
SOURCE_TEXTS.with(|s| {
let mut s = s.borrow_mut();
s.entry(file.to_path_buf())
.or_insert_with(|| Rc::from(text));
});
}
pub fn clear_sources() {
SOURCE_TEXTS.with(|s| s.borrow_mut().clear());
}
fn text_for(file: &Path) -> Option<Rc<str>> {
SOURCE_TEXTS.with(|s| s.borrow().get(file).cloned())
}
pub struct ResolvedPos {
pub file: String,
pub line: u64,
pub column: u64,
}
#[must_use]
pub fn resolve(file: Option<&Path>, offset: u32) -> Option<ResolvedPos> {
let file_path = file?;
let text = text_for(file_path)?;
let file = crate::path::dematerialize(file_path)
.to_string_lossy()
.into_owned();
let (line, column) = line_col(&text, offset);
Some(ResolvedPos { file, line, column })
}
fn line_col(text: &str, offset: u32) -> (u64, u64) {
let off = (offset as usize).min(text.len());
let head = &text.as_bytes()[..off];
let line = 1 + head.iter().filter(|b| **b == b'\n').count();
let bol = head.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
(line as u64, (off - bol) as u64 + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_col_matches_cppnix() {
let t = "{\n aaaaa = 1;\n bbbbb = 2;\n ccccc = 3;\n}\n";
assert_eq!(line_col(t, t.find("aaaaa").unwrap() as u32), (2, 3));
assert_eq!(line_col(t, t.find("bbbbb").unwrap() as u32), (3, 3));
assert_eq!(line_col(t, t.find("ccccc").unwrap() as u32), (4, 3));
assert_eq!(line_col(t, 0), (1, 1));
}
#[test]
fn line_col_columns_are_bytes_not_chars() {
let t = "{ \"é\" = 1; b = 2; }";
let b = t.find(" b =").unwrap() as u32 + 1;
assert_eq!(line_col(t, b), (1, u64::from(b) + 1));
assert!(t.chars().count() < t.len(), "fixture must be multi-byte");
}
#[test]
fn line_col_clamps_past_end() {
assert_eq!(line_col("ab\ncd", 9_999), (2, 3));
}
#[test]
fn resolve_none_for_unregistered_file() {
clear_sources();
assert!(resolve(Some(Path::new("/nowhere/x.nix")), 0).is_none());
}
#[test]
fn resolve_none_when_no_file() {
clear_sources();
register_source(None, "x = 1;");
assert!(resolve(None, 0).is_none());
}
#[test]
fn resolve_reports_file_and_cppnix_offset_pos() {
clear_sources();
let f = PathBuf::from("/nix/store/deadbeef-source/foo.nix");
register_source(Some(&f), "a = 1;\nbcd = 2;");
let p = resolve(Some(&f), 7).unwrap();
assert_eq!(p.file, "/nix/store/deadbeef-source/foo.nix");
assert_eq!(p.line, 2);
assert_eq!(p.column, 1);
}
}