use core::fmt;
use std::{num::NonZeroUsize, sync::Arc};
use parking_lot::{Mutex, RwLock};
use reflexo::hash::FxHashMap;
use reflexo::{ImmutPath, QueryRef};
use reflexo_vfs::{Bytes, FileId, FsProvider, TypstFileId};
use typst::{
diag::{FileError, FileResult},
syntax::Source,
};
type IncrQueryRef<S, E> = QueryRef<S, E, Option<S>>;
type FileQuery<T> = QueryRef<T, FileError>;
type IncrFileQuery<T> = IncrQueryRef<T, FileError>;
pub trait Revised {
fn last_accessed_rev(&self) -> NonZeroUsize;
}
pub struct SharedState<T> {
pub committed_revision: Option<usize>,
cache_entries: FxHashMap<TypstFileId, T>,
}
impl<T> fmt::Debug for SharedState<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SharedState")
.field("committed_revision", &self.committed_revision)
.finish()
}
}
impl<T> Default for SharedState<T> {
fn default() -> Self {
SharedState {
committed_revision: None,
cache_entries: FxHashMap::default(),
}
}
}
impl<T: Revised> SharedState<T> {
fn gc(&mut self) {
let committed = self.committed_revision.unwrap_or(0);
self.cache_entries
.retain(|_, v| committed.saturating_sub(v.last_accessed_rev().get()) <= 30);
}
}
pub struct SourceCache {
last_accessed_rev: NonZeroUsize,
fid: FileId,
source: IncrFileQuery<Source>,
buffer: FileQuery<Bytes>,
}
impl fmt::Debug for SourceCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceCache").finish()
}
}
impl Revised for SourceCache {
fn last_accessed_rev(&self) -> NonZeroUsize {
self.last_accessed_rev
}
}
pub struct SourceState {
pub revision: NonZeroUsize,
pub slots: Arc<Mutex<FxHashMap<TypstFileId, SourceCache>>>,
}
impl SourceState {
pub fn commit_impl(self, state: &mut SharedState<SourceCache>) {
log::debug!("drop source db revision {}", self.revision);
if let Ok(slots) = Arc::try_unwrap(self.slots) {
if state
.committed_revision
.map_or(false, |committed| committed >= self.revision.get())
{
return;
}
log::debug!("committing source db revision {}", self.revision);
state.committed_revision = Some(self.revision.get());
state.cache_entries = slots.into_inner();
state.gc();
}
}
}
#[derive(Clone)]
pub struct SourceDb {
pub revision: NonZeroUsize,
pub shared: Arc<RwLock<SharedState<SourceCache>>>,
pub slots: Arc<Mutex<FxHashMap<TypstFileId, SourceCache>>>,
pub do_reparse: bool,
}
impl fmt::Debug for SourceDb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceDb").finish()
}
}
impl SourceDb {
pub fn take_state(&mut self) -> SourceState {
SourceState {
revision: self.revision,
slots: std::mem::take(&mut self.slots),
}
}
pub fn set_do_reparse(&mut self, do_reparse: bool) {
self.do_reparse = do_reparse;
}
pub fn memory_usage(&self) -> usize {
let mut w = self.slots.lock().len() * core::mem::size_of::<SourceCache>();
w += self
.slots
.lock()
.iter()
.map(|(_, slot)| {
slot.source
.get_uninitialized()
.and_then(|e| e.as_ref().ok())
.map_or(16, |e| e.text().len() * 8)
+ slot
.buffer
.get_uninitialized()
.and_then(|e| e.as_ref().ok())
.map_or(16, |e| e.len())
})
.sum::<usize>();
w
}
pub fn iter_dependencies_dyn<'a>(
&'a self,
p: &'a impl FsProvider,
f: &mut dyn FnMut(ImmutPath),
) {
for slot in self.slots.lock().iter() {
f(p.file_path(slot.1.fid));
}
}
pub fn file(&self, id: TypstFileId, fid: FileId, p: &impl FsProvider) -> FileResult<Bytes> {
self.slot(id, fid, |slot| slot.buffer.compute(|| p.read(fid)).cloned())
}
pub fn source(&self, id: TypstFileId, fid: FileId, p: &impl FsProvider) -> FileResult<Source> {
self.slot(id, fid, |slot| {
slot.source
.compute_with_context(|prev| {
let content = p.read(fid)?;
let next = from_utf8_or_bom(&content)?.to_owned();
match prev {
Some(mut source) if self.do_reparse => {
source.replace(&next);
Ok(source)
}
_ => Ok(Source::new(id, next)),
}
})
.cloned()
})
}
fn slot<T>(&self, id: TypstFileId, fid: FileId, f: impl FnOnce(&SourceCache) -> T) -> T {
let mut slots = self.slots.lock();
f(slots.entry(id).or_insert_with(|| {
let state = self.shared.read();
let cache_entry = state.cache_entries.get(&id);
cache_entry
.map(|e| SourceCache {
last_accessed_rev: self.revision.max(e.last_accessed_rev),
fid,
source: IncrFileQuery::with_context(
e.source
.get_uninitialized()
.cloned()
.transpose()
.ok()
.flatten(),
),
buffer: FileQuery::default(),
})
.unwrap_or_else(|| SourceCache {
last_accessed_rev: self.revision,
fid,
source: IncrFileQuery::with_context(None),
buffer: FileQuery::default(),
})
}))
}
}
pub trait MergeCache: Sized {
fn merge(self, _other: Self) -> Self {
self
}
}
pub struct FontDb {}
pub struct PackageDb {}
fn from_utf8_or_bom(buf: &[u8]) -> FileResult<&str> {
Ok(std::str::from_utf8(if buf.starts_with(b"\xef\xbb\xbf") {
&buf[3..]
} else {
buf
})?)
}