use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
use crate::salsa::{Db, FileSet, FileText};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct FileId(u32);
#[derive(Clone)]
struct FileMeta {
path: Option<PathBuf>,
input: FileText,
}
#[derive(Clone, Default)]
struct VfsInner {
files: Vec<Option<FileMeta>>,
path_to_id: HashMap<PathBuf, FileId>,
input_to_id: HashMap<FileText, FileId>,
}
impl VfsInner {
fn meta(&self, id: FileId) -> Option<&FileMeta> {
self.files.get(id.0 as usize).and_then(|slot| slot.as_ref())
}
}
#[derive(Clone, Default)]
pub(crate) struct Vfs {
inner: Arc<RwLock<Arc<VfsInner>>>,
file_set: Arc<OnceLock<FileSet>>,
}
impl Vfs {
fn snapshot(&self) -> Arc<VfsInner> {
self.inner.read().expect("vfs lock poisoned").clone()
}
fn with_mut<R>(&self, f: impl FnOnce(&mut VfsInner) -> R) -> R {
let mut guard = self.inner.write().expect("vfs lock poisoned");
let mut next = (**guard).clone();
let out = f(&mut next);
*guard = Arc::new(next);
out
}
pub(crate) fn file_set(&self, db: &dyn Db) -> FileSet {
*self
.file_set
.get_or_init(|| FileSet::new(db, Arc::new(HashSet::new())))
}
pub(crate) fn id_for_path(&self, path: &Path) -> Option<FileId> {
self.snapshot().path_to_id.get(path).copied()
}
pub(crate) fn input_for_id(&self, id: FileId) -> Option<FileText> {
self.snapshot().meta(id).map(|meta| meta.input)
}
pub(crate) fn id_for_input(&self, input: FileText) -> Option<FileId> {
self.snapshot().input_to_id.get(&input).copied()
}
pub(crate) fn input_for_path(&self, path: &Path) -> Option<FileText> {
let snap = self.snapshot();
let id = *snap.path_to_id.get(path)?;
snap.meta(id).map(|meta| meta.input)
}
pub(crate) fn path_for_id(&self, id: FileId) -> Option<PathBuf> {
self.snapshot().meta(id).and_then(|meta| meta.path.clone())
}
pub(crate) fn path_for_input(&self, input: FileText) -> Option<PathBuf> {
let snap = self.snapshot();
let id = *snap.input_to_id.get(&input)?;
snap.meta(id).and_then(|meta| meta.path.clone())
}
pub(crate) fn cached_paths(&self) -> Vec<PathBuf> {
self.snapshot().path_to_id.keys().cloned().collect()
}
pub(crate) fn register(&self, path: Option<PathBuf>, input: FileText) -> FileId {
self.with_mut(|inner| {
let id = FileId(inner.files.len() as u32);
if let Some(path) = path.clone() {
inner.path_to_id.insert(path, id);
}
inner.input_to_id.insert(input, id);
inner.files.push(Some(FileMeta { path, input }));
id
})
}
pub(crate) fn remove_path(&self, path: &Path) -> Option<FileId> {
self.with_mut(|inner| {
let id = inner.path_to_id.remove(path)?;
let removed = inner
.files
.get_mut(id.0 as usize)
.and_then(|slot| slot.take());
if let Some(meta) = removed {
inner.input_to_id.remove(&meta.input);
}
Some(id)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::salsa::SalsaDb;
#[test]
fn register_assigns_distinct_ids_and_round_trips_lookups() {
let db = SalsaDb::default();
let vfs = Vfs::default();
let a = FileText::new(&db, None);
let b = FileText::new(&db, None);
let id_a = vfs.register(Some(PathBuf::from("/a.qmd")), a);
let id_b = vfs.register(Some(PathBuf::from("/b.qmd")), b);
assert_ne!(id_a, id_b);
assert_eq!(vfs.id_for_path(Path::new("/a.qmd")), Some(id_a));
assert!(vfs.input_for_id(id_a) == Some(a));
assert!(vfs.input_for_path(Path::new("/a.qmd")) == Some(a));
assert_eq!(vfs.path_for_id(id_a), Some(PathBuf::from("/a.qmd")));
assert_eq!(vfs.path_for_input(a), Some(PathBuf::from("/a.qmd")));
}
#[test]
fn evict_clears_lookups_and_reintern_mints_fresh_id() {
let db = SalsaDb::default();
let vfs = Vfs::default();
let a = FileText::new(&db, None);
let id1 = vfs.register(Some(PathBuf::from("/a.qmd")), a);
assert_eq!(vfs.remove_path(Path::new("/a.qmd")), Some(id1));
assert_eq!(vfs.id_for_path(Path::new("/a.qmd")), None);
assert!(vfs.input_for_id(id1).is_none());
assert_eq!(vfs.path_for_id(id1), None);
assert_eq!(vfs.path_for_input(a), None);
assert!(vfs.cached_paths().is_empty());
let b = FileText::new(&db, None);
let id2 = vfs.register(Some(PathBuf::from("/a.qmd")), b);
assert_ne!(id1, id2);
assert_eq!(vfs.id_for_path(Path::new("/a.qmd")), Some(id2));
}
#[test]
fn in_memory_buffers_get_distinct_pathless_ids() {
let db = SalsaDb::default();
let vfs = Vfs::default();
let a = FileText::new(&db, None);
let b = FileText::new(&db, None);
let id_a = vfs.register(None, a);
let id_b = vfs.register(None, b);
assert_ne!(id_a, id_b);
assert_eq!(vfs.path_for_id(id_a), None);
assert_eq!(vfs.path_for_input(a), None);
assert!(vfs.input_for_id(id_a) == Some(a));
}
}