mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 21:37:32 +00:00
feat: detect compilation-related vfs changes (#1199)
This commit is contained in:
parent
e4bf2e9e46
commit
d5ecf052d4
6 changed files with 113 additions and 70 deletions
|
|
@ -26,7 +26,7 @@ use crate::LspCompilerFeat;
|
|||
use tinymist_world::{
|
||||
vfs::{
|
||||
notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
|
||||
FsProvider, RevisingVfs,
|
||||
FileId, FsProvider, RevisingVfs,
|
||||
},
|
||||
CompilerFeat, CompilerUniverse, CompilerWorld, EntryReader, EntryState, TaskInputs, WorldDeps,
|
||||
};
|
||||
|
|
@ -99,6 +99,7 @@ impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
|
|||
snap,
|
||||
doc,
|
||||
warnings,
|
||||
deps: OnceLock::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +123,8 @@ pub struct CompiledArtifact<F: CompilerFeat> {
|
|||
pub warnings: EcoVec<SourceDiagnostic>,
|
||||
/// The compiled document.
|
||||
pub doc: SourceResult<Arc<TypstDocument>>,
|
||||
/// The depended files.
|
||||
pub deps: OnceLock<EcoVec<FileId>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
|
||||
|
|
@ -138,6 +141,7 @@ impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
|
|||
snap: self.snap.clone(),
|
||||
doc: self.doc.clone(),
|
||||
warnings: self.warnings.clone(),
|
||||
deps: self.deps.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -150,6 +154,17 @@ impl<F: CompilerFeat> CompiledArtifact<F> {
|
|||
.cloned()
|
||||
.or_else(|| self.snap.success_doc.clone())
|
||||
}
|
||||
|
||||
pub fn depended_files(&self) -> &EcoVec<FileId> {
|
||||
self.deps.get_or_init(|| {
|
||||
let mut deps = EcoVec::default();
|
||||
self.world.iter_dependencies(&mut |f| {
|
||||
deps.push(f);
|
||||
});
|
||||
|
||||
deps
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
|
||||
|
|
@ -225,6 +240,14 @@ impl CompileReasons {
|
|||
pub fn any(&self) -> bool {
|
||||
self.by_memory_events || self.by_fs_events || self.by_entry_update
|
||||
}
|
||||
|
||||
pub fn exclude(&self, excluded: Self) -> Self {
|
||||
Self {
|
||||
by_memory_events: self.by_memory_events && !excluded.by_memory_events,
|
||||
by_fs_events: self.by_fs_events && !excluded.by_fs_events,
|
||||
by_entry_update: self.by_entry_update && !excluded.by_entry_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn no_reason() -> CompileReasons {
|
||||
|
|
|
|||
|
|
@ -187,7 +187,11 @@ pub struct Vfs<M: PathAccessModel + Sized> {
|
|||
|
||||
impl<M: PathAccessModel + Sized> fmt::Debug for Vfs<M> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Vfs").finish()
|
||||
f.debug_struct("Vfs")
|
||||
.field("revision", &self.revision)
|
||||
.field("managed", &self.managed.lock().entries.size())
|
||||
.field("paths", &self.paths.lock().paths.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,10 +215,25 @@ impl<M: PathAccessModel + Clone + Sized> Vfs<M> {
|
|||
source_cache: self.source_cache.clone(),
|
||||
managed: Arc::new(Mutex::new(EntryMap::default())),
|
||||
paths: Arc::new(Mutex::new(PathMap::default())),
|
||||
revision: NonZeroUsize::new(1).expect("initial revision is 1"),
|
||||
revision: NonZeroUsize::new(2).expect("initial revision is 2"),
|
||||
access_model: self.access_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_clean_compile(&self, rev: usize, file_ids: &[FileId]) -> bool {
|
||||
let mut m = self.managed.lock();
|
||||
for id in file_ids {
|
||||
let Some(entry) = m.entries.get_mut(id) else {
|
||||
log::info!("Vfs(dirty, {id:?}): file id not found");
|
||||
return false;
|
||||
};
|
||||
if entry.changed_at == 0 || entry.changed_at >= rev {
|
||||
log::info!("Vfs(dirty, {id:?}): rev {rev:?} => {:?}", entry.changed_at);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: PathAccessModel + Sized> Vfs<M> {
|
||||
|
|
@ -247,7 +266,7 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
|
|||
source_cache: SourceCache::default(),
|
||||
managed: Arc::default(),
|
||||
paths: Arc::default(),
|
||||
revision: NonZeroUsize::new(1).expect("initial revision is 1"),
|
||||
revision: NonZeroUsize::new(2).expect("initial revision is 2"),
|
||||
access_model,
|
||||
}
|
||||
}
|
||||
|
|
@ -326,17 +345,20 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
|
|||
|
||||
/// Reads a file.
|
||||
pub fn read(&self, fid: TypstFileId) -> FileResult<Bytes> {
|
||||
let bytes = self.managed.lock().slot(fid, |entry| entry.bytes.clone());
|
||||
let bytes = self.managed.lock().slot(fid, |entry| {
|
||||
entry.changed_at = entry.changed_at.max(1);
|
||||
entry.bytes.clone()
|
||||
});
|
||||
|
||||
self.read_content(&bytes, fid).clone()
|
||||
}
|
||||
|
||||
/// Reads a source.
|
||||
pub fn source(&self, file_id: TypstFileId) -> FileResult<Source> {
|
||||
let (bytes, source) = self
|
||||
.managed
|
||||
.lock()
|
||||
.slot(file_id, |entry| (entry.bytes.clone(), entry.source.clone()));
|
||||
let (bytes, source) = self.managed.lock().slot(file_id, |entry| {
|
||||
entry.changed_at = entry.changed_at.max(1);
|
||||
(entry.bytes.clone(), entry.source.clone())
|
||||
});
|
||||
|
||||
let source = source.get_or_init(|| {
|
||||
let content = self
|
||||
|
|
@ -442,6 +464,7 @@ impl<M: PathAccessModel + Sized> RevisingVfs<'_, M> {
|
|||
fn invalidate_file_id(&mut self, file_id: TypstFileId) {
|
||||
self.view_changed = true;
|
||||
self.managed.slot(file_id, |e| {
|
||||
e.changed_at = self.inner.revision.get();
|
||||
e.bytes = Arc::default();
|
||||
e.source = Arc::default();
|
||||
});
|
||||
|
|
@ -524,6 +547,7 @@ type BytesQuery = Arc<OnceLock<(Option<ImmutPath>, usize, FileResult<Bytes>)>>;
|
|||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct VfsEntry {
|
||||
changed_at: usize,
|
||||
bytes: BytesQuery,
|
||||
source: Arc<OnceLock<FileResult<Source>>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,20 @@
|
|||
// use std::sync::Arc;
|
||||
|
||||
use core::fmt;
|
||||
use std::{num::NonZeroUsize, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use tinymist_std::hash::FxHashMap;
|
||||
use tinymist_std::QueryRef;
|
||||
use tinymist_std::{hash::FxHashMap, QueryRef};
|
||||
use tinymist_vfs::{Bytes, FsProvider, TypstFileId};
|
||||
use typst::{
|
||||
diag::{FileError, FileResult},
|
||||
syntax::Source,
|
||||
};
|
||||
|
||||
/// incrementally query a value from a self holding state
|
||||
type IncrQueryRef<S, E> = QueryRef<S, E, Option<S>>;
|
||||
use typst::diag::{FileError, FileResult};
|
||||
use typst::syntax::Source;
|
||||
|
||||
type FileQuery<T> = QueryRef<T, FileError>;
|
||||
type IncrFileQuery<T> = IncrQueryRef<T, FileError>;
|
||||
|
||||
pub trait Revised {
|
||||
fn last_accessed_rev(&self) -> NonZeroUsize;
|
||||
}
|
||||
|
||||
pub struct SourceCache {
|
||||
touched_by_compile: bool,
|
||||
fid: TypstFileId,
|
||||
source: IncrFileQuery<Source>,
|
||||
source: FileQuery<Source>,
|
||||
buffer: FileQuery<Bytes>,
|
||||
}
|
||||
|
||||
|
|
@ -103,22 +92,7 @@ impl SourceDb {
|
|||
/// See `Vfs::resolve_with_f` for more information.
|
||||
pub fn source(&self, fid: TypstFileId, p: &impl FsProvider) -> FileResult<Source> {
|
||||
self.slot(fid, |slot| {
|
||||
slot.source
|
||||
.compute_with_context(|prev| {
|
||||
let content = p.read(fid)?;
|
||||
let next = from_utf8_or_bom(&content)?.to_owned();
|
||||
|
||||
// otherwise reparse the source
|
||||
match prev {
|
||||
Some(mut source) => {
|
||||
source.replace(&next);
|
||||
Ok(source)
|
||||
}
|
||||
// Return a new source if we don't have a reparse feature or no prev
|
||||
_ => Ok(Source::new(fid, next)),
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
slot.source.compute(|| p.read_source(fid)).cloned()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +103,7 @@ impl SourceDb {
|
|||
let entry = slots.entry(fid).or_insert_with(|| SourceCache {
|
||||
touched_by_compile: self.is_compiling,
|
||||
fid,
|
||||
source: IncrFileQuery::with_context(None),
|
||||
source: FileQuery::default(),
|
||||
buffer: FileQuery::default(),
|
||||
});
|
||||
if self.is_compiling && !entry.touched_by_compile {
|
||||
|
|
@ -150,23 +124,3 @@ impl SourceDb {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MergeCache: Sized {
|
||||
fn merge(self, _other: Self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FontDb {}
|
||||
pub struct PackageDb {}
|
||||
|
||||
/// Convert a byte slice to a string, removing UTF-8 BOM if present.
|
||||
fn from_utf8_or_bom(buf: &[u8]) -> FileResult<&str> {
|
||||
Ok(std::str::from_utf8(if buf.starts_with(b"\xef\xbb\xbf") {
|
||||
// remove UTF-8 BOM
|
||||
&buf[3..]
|
||||
} else {
|
||||
// Assume UTF-8
|
||||
buf
|
||||
})?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,9 @@ impl<F: CompilerFeat> CompilerUniverse<F> {
|
|||
pub fn increment_revision<T>(&mut self, f: impl FnOnce(&mut RevisingUniverse<F>) -> T) -> T {
|
||||
f(&mut RevisingUniverse {
|
||||
vfs_revision: self.vfs.revision(),
|
||||
font_changed: false,
|
||||
font_revision: self.font_resolver.revision(),
|
||||
registry_changed: false,
|
||||
registry_revision: self.registry.revision(),
|
||||
view_changed: false,
|
||||
inner: self,
|
||||
|
|
@ -265,7 +267,9 @@ impl<F: CompilerFeat> EntryManager for CompilerUniverse<F> {
|
|||
pub struct RevisingUniverse<'a, F: CompilerFeat> {
|
||||
view_changed: bool,
|
||||
vfs_revision: NonZeroUsize,
|
||||
font_changed: bool,
|
||||
font_revision: Option<NonZeroUsize>,
|
||||
registry_changed: bool,
|
||||
registry_revision: Option<NonZeroUsize>,
|
||||
pub inner: &'a mut CompilerUniverse<F>,
|
||||
}
|
||||
|
|
@ -298,6 +302,7 @@ impl<F: CompilerFeat> Drop for RevisingUniverse<'_, F> {
|
|||
view_changed = true;
|
||||
|
||||
// The registry has changed affects the vfs cache.
|
||||
log::info!("resetting shadow registry_changed");
|
||||
self.vfs().reset_cache();
|
||||
}
|
||||
let view_changed = view_changed || self.vfs_changed();
|
||||
|
|
@ -316,10 +321,12 @@ impl<F: CompilerFeat> RevisingUniverse<'_, F> {
|
|||
}
|
||||
|
||||
pub fn set_fonts(&mut self, fonts: Arc<F::FontResolver>) {
|
||||
self.font_changed = true;
|
||||
self.inner.font_resolver = fonts;
|
||||
}
|
||||
|
||||
pub fn set_package(&mut self, packages: Arc<F::Registry>) {
|
||||
self.registry_changed = true;
|
||||
self.inner.registry = packages;
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +347,7 @@ impl<F: CompilerFeat> RevisingUniverse<'_, F> {
|
|||
// Resets the cache if the workspace root has changed.
|
||||
let root_changed = self.inner.entry.workspace_root() == state.workspace_root();
|
||||
if root_changed {
|
||||
log::info!("resetting shadow root_changed");
|
||||
self.vfs().reset_cache();
|
||||
}
|
||||
|
||||
|
|
@ -351,11 +359,12 @@ impl<F: CompilerFeat> RevisingUniverse<'_, F> {
|
|||
}
|
||||
|
||||
pub fn font_changed(&self) -> bool {
|
||||
is_revision_changed(self.font_revision, self.font_resolver.revision())
|
||||
self.font_changed && is_revision_changed(self.font_revision, self.font_resolver.revision())
|
||||
}
|
||||
|
||||
pub fn registry_changed(&self) -> bool {
|
||||
is_revision_changed(self.registry_revision, self.registry.revision())
|
||||
self.registry_changed
|
||||
&& is_revision_changed(self.registry_revision, self.registry.revision())
|
||||
}
|
||||
|
||||
pub fn vfs_changed(&self) -> bool {
|
||||
|
|
@ -447,6 +456,10 @@ impl<F: CompilerFeat> CompilerWorld<F> {
|
|||
self.source_db.take_state()
|
||||
}
|
||||
|
||||
pub fn vfs(&self) -> &Vfs<F::AccessModel> {
|
||||
&self.vfs
|
||||
}
|
||||
|
||||
/// Sets flag to indicate whether the compiler is currently compiling.
|
||||
/// Note: Since `CompilerWorld` can be cloned, you can clone the world and
|
||||
/// set the flag then to avoid affecting the original world.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ impl LspPreviewState {
|
|||
#[derive(Default)]
|
||||
pub struct ProjectStateExt {
|
||||
pub is_compiling: bool,
|
||||
pub last_compilation: Option<CompiledArtifact<LspCompilerFeat>>,
|
||||
}
|
||||
|
||||
/// LSP project compiler.
|
||||
|
|
@ -129,6 +130,7 @@ impl Project {
|
|||
let proj = self.state.projects().find(|p| p.id == compiled.id);
|
||||
if let Some(proj) = proj {
|
||||
proj.ext.is_compiling = false;
|
||||
proj.ext.last_compilation = Some(compiled.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +230,33 @@ impl CompileHandler<LspCompilerFeat, ProjectStateExt> for CompileHandlerImpl {
|
|||
continue;
|
||||
}
|
||||
|
||||
let reason = s.reason;
|
||||
|
||||
const VFS_SUB: CompileReasons = CompileReasons {
|
||||
by_memory_events: true,
|
||||
by_fs_events: true,
|
||||
by_entry_update: false,
|
||||
};
|
||||
|
||||
let is_vfs_sub = reason.any() && !reason.exclude(VFS_SUB).any();
|
||||
let id = &s.id;
|
||||
|
||||
if is_vfs_sub
|
||||
&& 'vfs_is_clean: {
|
||||
let Some(compilation) = &s.ext.last_compilation else {
|
||||
break 'vfs_is_clean false;
|
||||
};
|
||||
|
||||
let last_rev = compilation.world.vfs().revision();
|
||||
let deps = compilation.depended_files().clone();
|
||||
s.verse.vfs().is_clean_compile(last_rev.get(), &deps)
|
||||
}
|
||||
{
|
||||
log::info!("Project: skip compilation for {id:?} due to harmless vfs changes");
|
||||
s.reason = CompileReasons::default();
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(compile_fn) = s.may_compile(&c.handler) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -287,7 +316,7 @@ impl CompileHandler<LspCompilerFeat, ProjectStateExt> for CompileHandlerImpl {
|
|||
let mut n_rev = self.notified_revision.lock();
|
||||
if *n_rev >= snap.world.revision().get() {
|
||||
log::info!(
|
||||
"TypstActor: already notified for revision {} <= {n_rev}",
|
||||
"Project: already notified for revision {} <= {n_rev}",
|
||||
snap.world.revision(),
|
||||
);
|
||||
return;
|
||||
|
|
@ -407,11 +436,11 @@ impl QuerySnap {
|
|||
pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> anyhow::Result<T> {
|
||||
let world = self.snap.world;
|
||||
let Some(main) = world.main_id() else {
|
||||
error!("TypstActor: main file is not set");
|
||||
error!("Project: main file is not set");
|
||||
bail!("main file is not set");
|
||||
};
|
||||
world.source(main).map_err(|err| {
|
||||
info!("TypstActor: failed to prepare main file: {err:?}");
|
||||
info!("Project: failed to prepare main file: {err:?}");
|
||||
anyhow::anyhow!("failed to get source: {err}")
|
||||
})?;
|
||||
|
||||
|
|
|
|||
|
|
@ -286,15 +286,15 @@ impl LanguageState {
|
|||
mut state: ServiceState<T, T::S>,
|
||||
params: LspInterrupt,
|
||||
) -> anyhow::Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
log::info!("incoming interrupt: {params:?}");
|
||||
let _start = std::time::Instant::now();
|
||||
// log::info!("incoming interrupt: {params:?}");
|
||||
let Some(ready) = state.ready() else {
|
||||
log::info!("interrupted on not ready server");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
ready.project.interrupt(params);
|
||||
log::info!("interrupted in {:?}", start.elapsed());
|
||||
// log::info!("interrupted in {:?}", _start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue