diff --git a/crates/reflexo-typst/src/compile.rs b/crates/reflexo-typst/src/compile.rs index 1fb0341e1..69aefa983 100644 --- a/crates/reflexo-typst/src/compile.rs +++ b/crates/reflexo-typst/src/compile.rs @@ -574,7 +574,7 @@ impl CompileActor { // Actual a delayed memory event. reason = reason_by_mem(); } - verse.notify_fs_event(event) + verse.vfs().notify_fs_event(event) }); reason @@ -616,24 +616,17 @@ impl CompileActor { /// Apply memory changes to underlying compiler. fn apply_memory_changes(verse: &mut Revising>, event: MemoryEvent) { + let mut vfs = verse.vfs(); if matches!(event, MemoryEvent::Sync(..)) { - verse.reset_shadow(); + vfs.reset_shadow(); } match event { MemoryEvent::Update(event) | MemoryEvent::Sync(event) => { - for removes in event.removes { - let _ = verse.unmap_shadow(&removes); + for path in event.removes { + let _ = vfs.unmap_shadow(&path); } - for (p, t) in event.inserts { - let insert_file = match t.content().cloned() { - Ok(content) => content, - Err(err) => { - log::error!("CompileActor: read memory file at {p:?}: {err}"); - continue; - } - }; - - let _ = verse.map_shadow(&p, insert_file); + for (path, snap) in event.inserts { + let _ = vfs.map_shadow(&path, snap); } } } diff --git a/crates/reflexo-typst/src/watch.rs b/crates/reflexo-typst/src/watch.rs index d3de9f5e3..790bde930 100644 --- a/crates/reflexo-typst/src/watch.rs +++ b/crates/reflexo-typst/src/watch.rs @@ -16,9 +16,9 @@ use tokio::sync::mpsc; use typst::diag::FileError; use crate::vfs::{ - notify::{FileChangeSet, FileSnapshot, FilesystemEvent, NotifyMessage, UpstreamUpdateEvent}, + notify::{FilesystemEvent, NotifyMessage, UpstreamUpdateEvent}, system::SystemAccessModel, - PathAccessModel, + FileChangeSet, FileSnapshot, PathAccessModel, }; use crate::ImmutPath; diff --git a/crates/reflexo-vfs/src/lib.rs b/crates/reflexo-vfs/src/lib.rs index 1df514061..7da1cf6d2 100644 --- a/crates/reflexo-vfs/src/lib.rs +++ b/crates/reflexo-vfs/src/lib.rs @@ -17,6 +17,11 @@ pub mod system; /// [`Vfs`] will make a overlay access model over the provided dummy access /// model. pub mod dummy; + +/// Provides snapshot models +pub mod snapshot; +pub use snapshot::*; + /// Provides notify access model which retrieves file system events and changes /// from some notify backend. pub mod notify; @@ -39,6 +44,7 @@ pub use typst::syntax::FileId as TypstFileId; pub use reflexo::time::Time; pub use reflexo::ImmutPath; +use typst::syntax::Source; use core::fmt; use std::sync::OnceLock; @@ -126,12 +132,14 @@ pub trait FsProvider { #[derive(Debug, Clone, Default)] struct VfsEntry { bytes: Arc>>, + source: Arc>>, } /// Create a new `Vfs` harnessing over the given `access_model` specific for /// `reflexo_world::CompilerWorld`. With vfs, we can minimize the /// implementation overhead for [`AccessModel`] trait. pub struct Vfs { + touched_by_compile: bool, managed: Arc>>, // access_model: TraceAccessModel>, /// The wrapped access model. @@ -147,6 +155,7 @@ impl fmt::Debug for Vfs { impl Vfs { pub fn snapshot(&self) -> Self { Self { + touched_by_compile: self.touched_by_compile, managed: self.managed.clone(), access_model: self.access_model.clone(), } @@ -180,6 +189,7 @@ impl Vfs { // let access_model = TraceAccessModel::new(access_model); Self { + touched_by_compile: false, managed: Arc::default(), access_model, } @@ -201,14 +211,6 @@ impl Vfs { self.access_model.inner.resolver.path_for_id(id) } - /// Reset the shadowing files in [`OverlayAccessModel`]. - /// - /// Note: This function is independent from [`Vfs::reset`]. - pub fn reset_shadow(&mut self) { - self.access_model.clear_shadow(); - self.access_model.inner.inner.clear_shadow(); - } - /// Get paths to all the shadowing files in [`OverlayAccessModel`]. pub fn shadow_paths(&self) -> Vec { self.access_model.inner.inner.file_paths() @@ -219,61 +221,136 @@ impl Vfs { self.access_model.file_paths() } + /// Returns the overall memory usage for the stored files. + pub fn memory_usage(&self) -> usize { + 0 + } + + pub fn revise(&mut self) -> RevisingVfs { + RevisingVfs { inner: self } + } + + pub fn reset_shadow(&mut self) { + self.revise().reset_shadow(); + } + + pub fn map_shadow(&mut self, path: &Path, snap: FileSnapshot) -> FileResult<()> { + self.revise().map_shadow(path, snap) + } + + pub fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { + self.revise().unmap_shadow(path) + } + + pub fn map_shadow_by_id(&mut self, file_id: TypstFileId, snap: FileSnapshot) -> FileResult<()> { + self.revise().map_shadow_by_id(file_id, snap) + } + + pub fn remove_shadow_by_id(&mut self, file_id: TypstFileId) { + self.revise().remove_shadow_by_id(file_id); + } + + pub fn notify_fs_event(&mut self, event: FilesystemEvent) { + self.revise().notify_fs_event(event); + } + + /// Read a file. + pub fn read(&self, path: TypstFileId) -> FileResult { + let entry = self.slot(path, |entry| entry.bytes.clone()); + + let content = entry.get_or_init(|| self.access_model.content(path)); + content.clone() + } + + /// Read a source. + pub fn source(&self, path: TypstFileId) -> FileResult { + let (bytes, source) = self.slot(path, |entry| (entry.bytes.clone(), entry.source.clone())); + + let source = source.get_or_init(|| { + let content = bytes + .get_or_init(|| self.access_model.content(path)) + .as_ref() + .map_err(Clone::clone)?; + + let content = std::str::from_utf8(content).map_err(|_| FileError::InvalidUtf8)?; + + Ok(Source::new(path, content.into())) + }); + + source.clone() + } + + /// Read a slot. + #[inline(always)] + fn slot(&self, path: TypstFileId, f: impl FnOnce(&mut VfsEntry) -> T) -> T { + let mut m = self.managed.lock(); + + if let Some(entry) = m.get_mut(&path) { + f(entry) + } else { + let mut entry = VfsEntry::default(); + let res = f(&mut entry); + m.insert_mut(path, entry); + res + } + } +} + +pub struct RevisingVfs<'a, M: PathAccessModel + Sized> { + inner: &'a mut Vfs, +} + +impl RevisingVfs<'_, M> { + pub fn vfs(&mut self) -> &mut Vfs { + self.inner + } + + fn access_model(&mut self) -> &mut VfsAccessModel { + &mut self.inner.access_model + } + + /// Reset the shadowing files in [`OverlayAccessModel`]. + /// + /// Note: This function is independent from [`Vfs::reset`]. + pub fn reset_shadow(&mut self) { + self.access_model().clear_shadow(); + self.access_model().inner.inner.clear_shadow(); + } + /// Add a shadowing file to the [`OverlayAccessModel`]. - pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> { - self.access_model + pub fn map_shadow(&mut self, path: &Path, snap: FileSnapshot) -> FileResult<()> { + self.access_model() .inner .inner - .add_file(path, content, |c| c.into()); + .add_file(path, snap, |c| c.into()); Ok(()) } /// Remove a shadowing file from the [`OverlayAccessModel`]. - pub fn remove_shadow(&mut self, path: &Path) { - self.access_model.inner.inner.remove_file(path); + pub fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { + self.access_model().inner.inner.remove_file(path); + + Ok(()) } /// Add a shadowing file to the [`OverlayAccessModel`] by file id. - pub fn map_shadow_by_id(&mut self, file_id: TypstFileId, content: Bytes) -> FileResult<()> { - self.access_model.add_file(&file_id, content, |c| *c); + pub fn map_shadow_by_id(&mut self, file_id: TypstFileId, snap: FileSnapshot) -> FileResult<()> { + self.access_model().add_file(&file_id, snap, |c| *c); Ok(()) } /// Remove a shadowing file from the [`OverlayAccessModel`] by file id. pub fn remove_shadow_by_id(&mut self, file_id: TypstFileId) { - self.access_model.remove_file(&file_id); + self.access_model().remove_file(&file_id); } /// Let the vfs notify the access model with a filesystem event. /// /// See [`NotifyAccessModel`] for more information. pub fn notify_fs_event(&mut self, event: FilesystemEvent) { - self.access_model.inner.inner.inner.notify(event); - } - - /// Returns the overall memory usage for the stored files. - pub fn memory_usage(&self) -> usize { - 0 - } - - /// Read a file. - pub fn read(&self, path: TypstFileId) -> FileResult { - let entry = { - let mut m = self.managed.lock(); - - if let Some(entry) = m.get(&path) { - entry.bytes.clone() - } else { - let entry = VfsEntry::default(); - m.insert_mut(path, entry.clone()); - entry.bytes.clone() - } - }; - - let content = entry.get_or_init(|| self.access_model.content(path)); - content.clone() + self.access_model().inner.inner.inner.notify(event); } } diff --git a/crates/reflexo-vfs/src/notify.rs b/crates/reflexo-vfs/src/notify.rs index 95087d6c8..06e03b6e1 100644 --- a/crates/reflexo-vfs/src/notify.rs +++ b/crates/reflexo-vfs/src/notify.rs @@ -1,115 +1,9 @@ -use core::fmt; use std::path::Path; use rpds::RedBlackTreeMapSync; -use typst::diag::{FileError, FileResult}; +use typst::diag::FileResult; -use crate::{Bytes, ImmutPath, PathAccessModel}; - -/// A file snapshot that is notified by some external source -/// -/// Note: The error is boxed to avoid large stack size -#[derive(Clone, PartialEq, Eq)] -pub struct FileSnapshot(Result>); - -impl fmt::Debug for FileSnapshot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0.as_ref() { - Ok(v) => f - .debug_struct("FileSnapshot") - .field("content", &FileContent { len: v.len() }) - .finish(), - Err(e) => f.debug_struct("FileSnapshot").field("error", &e).finish(), - } - } -} - -impl FileSnapshot { - /// content of the file - #[inline] - #[track_caller] - pub fn content(&self) -> FileResult<&Bytes> { - self.0.as_ref().map_err(|e| *e.clone()) - } - - /// Whether the related file is a file - #[inline] - #[track_caller] - pub fn is_file(&self) -> FileResult { - self.content().map(|_| true) - } -} - -impl std::ops::Deref for FileSnapshot { - type Target = Result>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for FileSnapshot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Convenient function to create a [`FileSnapshot`] from tuple -impl From> for FileSnapshot { - fn from(result: FileResult) -> Self { - Self(result.map_err(Box::new)) - } -} - -/// A set of changes to the filesystem. -/// -/// The correct order of applying changes is: -/// 1. Remove files -/// 2. Upsert (Insert or Update) files -#[derive(Debug, Clone, Default)] -pub struct FileChangeSet { - /// Files to remove - pub removes: Vec, - /// Files to insert or update - pub inserts: Vec<(ImmutPath, FileSnapshot)>, -} - -impl FileChangeSet { - /// Create a new empty changeset - pub fn is_empty(&self) -> bool { - self.inserts.is_empty() && self.removes.is_empty() - } - - /// Create a new changeset with removing files - pub fn new_removes(removes: Vec) -> Self { - Self { - removes, - inserts: vec![], - } - } - - /// Create a new changeset with inserting files - pub fn new_inserts(inserts: Vec<(ImmutPath, FileSnapshot)>) -> Self { - Self { - removes: vec![], - inserts, - } - } - - /// Utility function to insert a possible file to insert or update - pub fn may_insert(&mut self, v: Option<(ImmutPath, FileSnapshot)>) { - if let Some(v) = v { - self.inserts.push(v); - } - } - - /// Utility function to insert multiple possible files to insert or update - pub fn may_extend(&mut self, v: Option>) { - if let Some(v) = v { - self.inserts.extend(v); - } - } -} +use crate::{Bytes, FileChangeSet, FileSnapshot, ImmutPath, PathAccessModel}; /// A memory event that is notified by some external source #[derive(Debug)] @@ -241,9 +135,3 @@ impl PathAccessModel for NotifyAccessModel { self.inner.content(src) } } - -#[derive(Debug)] -#[allow(dead_code)] -struct FileContent { - len: usize, -} diff --git a/crates/reflexo-vfs/src/overlay.rs b/crates/reflexo-vfs/src/overlay.rs index aeed15e00..8c6b1747c 100644 --- a/crates/reflexo-vfs/src/overlay.rs +++ b/crates/reflexo-vfs/src/overlay.rs @@ -4,13 +4,13 @@ use reflexo::ImmutPath; use rpds::RedBlackTreeMapSync; use typst::diag::FileResult; -use crate::{AccessModel, Bytes, PathAccessModel, TypstFileId}; +use crate::{AccessModel, Bytes, FileSnapshot, PathAccessModel, TypstFileId}; /// Provides overlay access model which allows to shadow the underlying access /// model with memory contents. #[derive(Default, Debug, Clone)] pub struct OverlayAccessModel { - files: RedBlackTreeMapSync, + files: RedBlackTreeMapSync, /// The underlying access model pub inner: M, } @@ -45,16 +45,20 @@ impl OverlayAccessModel { } /// Add a shadow file to the [`OverlayAccessModel`] - pub fn add_file(&mut self, path: &Q, content: Bytes, cast: impl Fn(&Q) -> K) - where + pub fn add_file( + &mut self, + path: &Q, + snap: FileSnapshot, + cast: impl Fn(&Q) -> K, + ) where K: Borrow, { match self.files.get_mut(path) { Some(e) => { - *e = content; + *e = snap; } None => { - self.files.insert_mut(cast(path), content); + self.files.insert_mut(cast(path), snap); } } } @@ -71,7 +75,7 @@ impl OverlayAccessModel { impl PathAccessModel for OverlayAccessModel { fn content(&self, src: &Path) -> FileResult { if let Some(content) = self.files.get(src) { - return Ok(content.clone()); + return content.content().cloned(); } self.inner.content(src) @@ -85,7 +89,7 @@ impl AccessModel for OverlayAccessModel { fn content(&self, src: TypstFileId) -> FileResult { if let Some(content) = self.files.get(&src) { - return Ok(content.clone()); + return content.content().cloned(); } self.inner.content(src) diff --git a/crates/reflexo-vfs/src/snapshot.rs b/crates/reflexo-vfs/src/snapshot.rs new file mode 100644 index 000000000..82fd6bf54 --- /dev/null +++ b/crates/reflexo-vfs/src/snapshot.rs @@ -0,0 +1,116 @@ +use core::fmt; + +use typst::diag::{FileError, FileResult}; + +use crate::{Bytes, ImmutPath}; + +/// A file snapshot that is notified by some external source +/// +/// Note: The error is boxed to avoid large stack size +#[derive(Clone, PartialEq, Eq)] +pub struct FileSnapshot(Result>); + +#[derive(Debug)] +#[allow(dead_code)] +struct FileContent { + len: usize, +} + +impl fmt::Debug for FileSnapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0.as_ref() { + Ok(v) => f + .debug_struct("FileSnapshot") + .field("content", &FileContent { len: v.len() }) + .finish(), + Err(e) => f.debug_struct("FileSnapshot").field("error", &e).finish(), + } + } +} + +impl FileSnapshot { + /// content of the file + #[inline] + #[track_caller] + pub fn content(&self) -> FileResult<&Bytes> { + self.0.as_ref().map_err(|e| *e.clone()) + } + + /// Whether the related file is a file + #[inline] + #[track_caller] + pub fn is_file(&self) -> FileResult { + self.content().map(|_| true) + } +} + +impl std::ops::Deref for FileSnapshot { + type Target = Result>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for FileSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Convenient function to create a [`FileSnapshot`] from tuple +impl From> for FileSnapshot { + fn from(result: FileResult) -> Self { + Self(result.map_err(Box::new)) + } +} + +/// A set of changes to the filesystem. +/// +/// The correct order of applying changes is: +/// 1. Remove files +/// 2. Upsert (Insert or Update) files +#[derive(Debug, Clone, Default)] +pub struct FileChangeSet { + /// Files to remove + pub removes: Vec, + /// Files to insert or update + pub inserts: Vec<(ImmutPath, FileSnapshot)>, +} + +impl FileChangeSet { + /// Create a new empty changeset + pub fn is_empty(&self) -> bool { + self.inserts.is_empty() && self.removes.is_empty() + } + + /// Create a new changeset with removing files + pub fn new_removes(removes: Vec) -> Self { + Self { + removes, + inserts: vec![], + } + } + + /// Create a new changeset with inserting files + pub fn new_inserts(inserts: Vec<(ImmutPath, FileSnapshot)>) -> Self { + Self { + removes: vec![], + inserts, + } + } + + /// Utility function to insert a possible file to insert or update + pub fn may_insert(&mut self, v: Option<(ImmutPath, FileSnapshot)>) { + if let Some(v) = v { + self.inserts.push(v); + } + } + + /// Utility function to insert multiple possible files to insert or update + pub fn may_extend(&mut self, v: Option>) { + if let Some(v) = v { + self.inserts.extend(v); + } + } +} diff --git a/crates/reflexo-world/src/world.rs b/crates/reflexo-world/src/world.rs index 1a702097a..04c6452f7 100644 --- a/crates/reflexo-world/src/world.rs +++ b/crates/reflexo-world/src/world.rs @@ -8,8 +8,8 @@ use std::{ use chrono::{DateTime, Datelike, Local}; use parking_lot::RwLock; use reflexo::error::prelude::*; -use reflexo_vfs::{notify::FilesystemEvent, PathResolution, Vfs, WorkspaceResolver}; use reflexo_vfs::{FsProvider, TypstFileId}; +use reflexo_vfs::{PathResolution, RevisingVfs, Vfs, WorkspaceResolver}; use typst::{ diag::{eco_format, At, EcoString, FileError, FileResult, SourceResult}, foundations::{Bytes, Datetime, Dict}, @@ -54,29 +54,26 @@ impl std::ops::DerefMut for Revising<'_, T> { } impl Revising<'_, CompilerUniverse> { - pub fn vfs(&mut self) -> &mut Vfs { - &mut self.inner.vfs + pub fn vfs(&mut self) -> RevisingVfs<'_, F::AccessModel> { + self.vfs.revise() } - /// Let the vfs notify the access model with a filesystem event. - /// - /// See `reflexo_vfs::NotifyAccessModel` for more information. - pub fn notify_fs_event(&mut self, event: FilesystemEvent) { - self.inner.vfs.notify_fs_event(event); - } + // pub fn notify_fs_event(&mut self, event: FilesystemEvent) { + // self.inner.vfs.notify_fs_event(event); + // } - pub fn reset_shadow(&mut self) { - self.inner.vfs.reset_shadow() - } + // pub fn reset_shadow(&mut self) { + // self.inner.vfs.reset_shadow() + // } - pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> { - self.inner.vfs.map_shadow(path, content) - } + // pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> { + // self.inner.vfs.map_shadow(path, content) + // } - pub fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { - self.inner.vfs.remove_shadow(path); - Ok(()) - } + // pub fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { + // self.inner.vfs.remove_shadow(path); + // Ok(()) + // } /// Set the inputs for the compiler. pub fn set_inputs(&mut self, inputs: Arc>) { @@ -283,20 +280,17 @@ impl ShadowApi for CompilerUniverse { #[inline] fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> { - self.increment_revision(|this| this.vfs().map_shadow(path, content)) + self.increment_revision(|this| this.vfs().map_shadow(path, Ok(content).into())) } #[inline] fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { - self.increment_revision(|this| { - this.vfs().remove_shadow(path); - Ok(()) - }) + self.increment_revision(|this| this.vfs().unmap_shadow(path)) } #[inline] fn map_shadow_by_id(&mut self, file_id: FileId, content: Bytes) -> FileResult<()> { - self.increment_revision(|this| this.vfs().map_shadow_by_id(file_id, content)) + self.increment_revision(|this| this.vfs().map_shadow_by_id(file_id, Ok(content).into())) } #[inline] @@ -455,18 +449,17 @@ impl ShadowApi for CompilerWorld { #[inline] fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> { - self.vfs.map_shadow(path, content) + self.vfs.map_shadow(path, Ok(content).into()) } #[inline] fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> { - self.vfs.remove_shadow(path); - Ok(()) + self.vfs.unmap_shadow(path) } #[inline] fn map_shadow_by_id(&mut self, file_id: TypstFileId, content: Bytes) -> FileResult<()> { - self.vfs.map_shadow_by_id(file_id, content) + self.vfs.map_shadow_by_id(file_id, Ok(content).into()) } #[inline] @@ -521,12 +514,12 @@ impl World for CompilerWorld { return Ok(DETACH_SOURCE.clone()); } - self.source_db.source(id, self) + self.vfs.source(id) } /// Try to access the specified file. fn file(&self, id: FileId) -> FileResult { - self.source_db.file(id, self) + self.vfs.read(id) } /// Get the current date.