feat: run project compilations on main thread (#1197)

* dev: handle compile interrupt

* dev: remove cache task use

* feat: create project crate again

dev: changes

feat: delete unused code

fix: errors

fix: errors

* feat: extra compilation is not needed

* dev: implement all todos

* fix: make event queue unbounded

* fix: make preview work again

* feat: event-driven recompilation

* feat: evict vfs cache

* feat: update evict strategy

* feat: remove lock update
This commit is contained in:
Myriad-Dreamin 2025-01-19 22:28:38 +08:00 committed by GitHub
parent 02a14c9cc9
commit e4bf2e9e46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1895 additions and 1778 deletions

2
Cargo.lock generated
View file

@ -4105,6 +4105,7 @@ dependencies = [
"parking_lot",
"pathdiff",
"rayon",
"reflexo-typst",
"semver",
"serde",
"serde_json",
@ -4254,7 +4255,6 @@ dependencies = [
"tar",
"tinymist-std",
"tinymist-vfs",
"toml",
"typst",
"typst-assets",
"wasm-bindgen",

View file

@ -5,7 +5,7 @@ use std::{
thread,
};
use crossbeam_channel::{bounded, Receiver, Sender};
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
use crate::{Connection, ConnectionRx, ConnectionTx, Message};
@ -67,7 +67,7 @@ pub fn with_stdio_transport(
};
let o = || std::io::stdout().lock();
let (event_sender, event_receiver) = bounded::<crate::Event>(10);
let (event_sender, event_receiver) = unbounded::<crate::Event>();
// Create the transport. Includes the stdio (stdin and stdout) versions but this
// could also be implemented to use sockets or HTTP.

View file

@ -23,6 +23,7 @@ parking_lot.workspace = true
pathdiff.workspace = true
tokio.workspace = true
rayon.workspace = true
reflexo-typst.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -0,0 +1,793 @@
//! Project Model for tinymist
//!
//! The [`ProjectCompiler`] implementation borrowed from typst.ts.
//!
//! Please check `tinymist::actor::typ_client` for architecture details.
#![allow(missing_docs)]
use core::fmt;
use std::{
collections::HashSet,
path::Path,
sync::{Arc, OnceLock},
};
use ecow::{EcoString, EcoVec};
use reflexo_typst::{
features::{CompileFeature, FeatureSet, WITH_COMPILING_STATUS_FEATURE},
CompileEnv, CompileReport, Compiler, TypstDocument,
};
use tinymist_std::error::prelude::ZResult;
use tokio::sync::mpsc;
use typst::diag::{SourceDiagnostic, SourceResult};
use crate::LspCompilerFeat;
use tinymist_world::{
vfs::{
notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
FsProvider, RevisingVfs,
},
CompilerFeat, CompilerUniverse, CompilerWorld, EntryReader, EntryState, TaskInputs, WorldDeps,
};
/// LSP interrupt.
pub type LspInterrupt = Interrupt<LspCompilerFeat>;
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct ProjectInsId(EcoString);
/// A signal that possibly triggers an export.
///
/// Whether to export depends on the current state of the document and the user
/// settings.
#[derive(Debug, Clone, Copy)]
pub struct ExportSignal {
/// Whether the revision is annotated by memory events.
pub by_mem_events: bool,
/// Whether the revision is annotated by file system events.
pub by_fs_events: bool,
/// Whether the revision is annotated by entry update.
pub by_entry_update: bool,
}
pub struct CompileSnapshot<F: CompilerFeat> {
/// The project id.
pub id: ProjectInsId,
/// The export signal for the document.
pub signal: ExportSignal,
/// Using env
pub env: CompileEnv,
/// Using world
pub world: CompilerWorld<F>,
/// The last successfully compiled document.
pub success_doc: Option<Arc<TypstDocument>>,
}
impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
pub fn task(mut self, inputs: TaskInputs) -> Self {
'check_changed: {
if let Some(entry) = &inputs.entry {
if *entry != self.world.entry_state() {
break 'check_changed;
}
}
if let Some(inputs) = &inputs.inputs {
if inputs.clone() != self.world.inputs() {
break 'check_changed;
}
}
return self;
};
self.world = self.world.task(inputs);
self
}
pub fn compile(self) -> CompiledArtifact<F> {
let mut snap = self;
snap.world.set_is_compiling(true);
let warned = std::marker::PhantomData.compile(&snap.world, &mut snap.env);
snap.world.set_is_compiling(false);
let (doc, warnings) = match warned {
Ok(doc) => (Ok(doc.output), doc.warnings),
Err(err) => (Err(err), EcoVec::default()),
};
CompiledArtifact {
snap,
doc,
warnings,
}
}
}
impl<F: CompilerFeat> Clone for CompileSnapshot<F> {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
signal: self.signal,
env: self.env.clone(),
world: self.world.clone(),
success_doc: self.success_doc.clone(),
}
}
}
pub struct CompiledArtifact<F: CompilerFeat> {
/// The used snapshot.
pub snap: CompileSnapshot<F>,
/// The diagnostics of the document.
pub warnings: EcoVec<SourceDiagnostic>,
/// The compiled document.
pub doc: SourceResult<Arc<TypstDocument>>,
}
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
type Target = CompileSnapshot<F>;
fn deref(&self) -> &Self::Target {
&self.snap
}
}
impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
fn clone(&self) -> Self {
Self {
snap: self.snap.clone(),
doc: self.doc.clone(),
warnings: self.warnings.clone(),
}
}
}
impl<F: CompilerFeat> CompiledArtifact<F> {
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
self.doc
.as_ref()
.ok()
.cloned()
.or_else(|| self.snap.success_doc.clone())
}
}
pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
// todo: notify project specific compile
fn notify_compile(&self, res: &CompiledArtifact<F>, rep: CompileReport);
fn status(&self, revision: usize, id: &ProjectInsId, rep: CompileReport);
}
/// No need so no compilation.
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
for std::marker::PhantomData<fn(F, Ext)>
{
fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
log::info!("ProjectHandle: no need to compile");
}
fn notify_compile(&self, _res: &CompiledArtifact<F>, _rep: CompileReport) {}
fn status(&self, _revision: usize, _id: &ProjectInsId, _rep: CompileReport) {}
}
pub enum Interrupt<F: CompilerFeat> {
/// Compile anyway.
Compile(ProjectInsId),
/// Settle a dedicated project.
Settle(ProjectInsId),
/// Compiled from computing thread.
Compiled(CompiledArtifact<F>),
/// Change the watching entry.
ChangeTask(ProjectInsId, TaskInputs),
/// Font changes.
Font(Arc<F::FontResolver>),
/// Memory file changes.
Memory(MemoryEvent),
/// File system event.
Fs(FilesystemEvent),
}
impl fmt::Debug for Interrupt<LspCompilerFeat> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id),
Interrupt::ChangeTask(id, change) => {
write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
}
Interrupt::Font(..) => write!(f, "Font(..)"),
Interrupt::Memory(..) => write!(f, "Memory(..)"),
Interrupt::Fs(..) => write!(f, "Fs(..)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CompileReasons {
/// The snapshot is taken by the memory editing events.
pub by_memory_events: bool,
/// The snapshot is taken by the file system events.
pub by_fs_events: bool,
/// The snapshot is taken by the entry change.
pub by_entry_update: bool,
}
impl CompileReasons {
/// Merge two reasons.
pub fn see(&mut self, reason: CompileReasons) {
self.by_memory_events |= reason.by_memory_events;
self.by_fs_events |= reason.by_fs_events;
self.by_entry_update |= reason.by_entry_update;
}
/// Whether there is any reason to compile.
pub fn any(&self) -> bool {
self.by_memory_events || self.by_fs_events || self.by_entry_update
}
}
fn no_reason() -> CompileReasons {
CompileReasons::default()
}
fn reason_by_mem() -> CompileReasons {
CompileReasons {
by_memory_events: true,
..CompileReasons::default()
}
}
fn reason_by_fs() -> CompileReasons {
CompileReasons {
by_fs_events: true,
..CompileReasons::default()
}
}
fn reason_by_entry_change() -> CompileReasons {
CompileReasons {
by_entry_update: true,
..CompileReasons::default()
}
}
/// A tagged memory event with logical tick.
struct TaggedMemoryEvent {
/// The logical tick when the event is received.
logical_tick: usize,
/// The memory event happened.
event: MemoryEvent,
}
pub struct CompileServerOpts<F: CompilerFeat, Ext> {
pub handler: Arc<dyn CompileHandler<F, Ext>>,
pub feature_set: FeatureSet,
pub enable_watch: bool,
}
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
fn default() -> Self {
Self {
handler: Arc::new(std::marker::PhantomData),
feature_set: Default::default(),
enable_watch: false,
}
}
}
/// The synchronous compiler that runs on one project or multiple projects.
pub struct ProjectCompiler<F: CompilerFeat, Ext> {
/// The compilation handle.
pub handler: Arc<dyn CompileHandler<F, Ext>>,
/// Channel for sending interrupts to the compiler actor.
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
/// Whether to enable file system watching.
pub enable_watch: bool,
/// The current logical tick.
logical_tick: usize,
/// Last logical tick when invalidation is caused by shadow update.
dirty_shadow_logical_tick: usize,
/// Estimated latest set of shadow files.
estimated_shadow_files: HashSet<Arc<Path>>,
/// The primary state.
pub primary: ProjectState<F, Ext>,
/// The states for dedicate tasks
pub dedicates: Vec<ProjectState<F, Ext>>,
}
impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
/// Create a compiler with options
pub fn new(
verse: CompilerUniverse<F>,
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
CompileServerOpts {
handler,
feature_set,
enable_watch,
}: CompileServerOpts<F, Ext>,
) -> Self {
let primary = Self::create_project(
ProjectInsId("primary".into()),
verse,
handler.clone(),
dep_tx.clone(),
feature_set.clone(),
);
Self {
handler,
dep_tx,
enable_watch,
logical_tick: 1,
dirty_shadow_logical_tick: 0,
estimated_shadow_files: Default::default(),
primary,
dedicates: vec![],
}
}
fn create_project(
id: ProjectInsId,
verse: CompilerUniverse<F>,
handler: Arc<dyn CompileHandler<F, Ext>>,
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
feature_set: FeatureSet,
) -> ProjectState<F, Ext> {
ProjectState {
id,
ext: Default::default(),
verse,
reason: no_reason(),
snapshot: None,
handler,
dep_tx,
compilation: OnceLock::default(),
latest_doc: None,
latest_success_doc: None,
once_feature_set: Arc::new(feature_set.clone()),
watch_feature_set: Arc::new(
feature_set
.clone()
.configure(&WITH_COMPILING_STATUS_FEATURE, true),
),
committed_revision: 0,
}
}
pub fn process(&mut self, intr: Interrupt<F>) {
// todo: evcit cache
self.process_inner(intr);
// Customized Project Compilation Handler
self.handler.clone().on_any_compile_reason(self);
}
pub fn snapshot(&mut self) -> CompileSnapshot<F> {
self.primary.snapshot()
}
/// Compile the document once.
pub fn compile_once(&mut self) -> CompiledArtifact<F> {
let snap = self.primary.make_snapshot(true);
ProjectState::run_compile(self.handler.clone(), snap)()
}
/// Apply delayed memory changes to underlying compiler.
fn apply_delayed_memory_changes(
verse: &mut RevisingVfs<'_, F::AccessModel>,
dirty_shadow_logical_tick: &mut usize,
event: &Option<UpstreamUpdateEvent>,
) -> Option<()> {
// Handle delayed upstream update event before applying file system changes
if let Some(event) = event {
let TaggedMemoryEvent {
logical_tick,
event,
} = event.opaque.as_ref().downcast_ref()?;
// Recovery from dirty shadow state.
if logical_tick == dirty_shadow_logical_tick {
*dirty_shadow_logical_tick = 0;
}
Self::apply_memory_changes(verse, event.clone());
}
Some(())
}
/// Apply memory changes to underlying compiler.
fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
if matches!(event, MemoryEvent::Sync(..)) {
vfs.reset_shadow();
}
match event {
MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
for path in event.removes {
let _ = vfs.unmap_shadow(&path);
}
for (path, snap) in event.inserts {
let _ = vfs.map_shadow(&path, snap);
}
}
}
}
fn find_project<'a>(
primary: &'a mut ProjectState<F, Ext>,
dedicates: &'a mut [ProjectState<F, Ext>],
id: &ProjectInsId,
) -> &'a mut ProjectState<F, Ext> {
if id == &primary.id {
return primary;
}
dedicates.iter_mut().find(|e| e.id == *id).unwrap()
}
pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectState<F, Ext>> {
std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
}
fn process_inner(&mut self, intr: Interrupt<F>) {
match intr {
Interrupt::Compile(id) => {
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
// Increment the revision anyway.
proj.verse.increment_revision(|verse| {
verse.flush();
});
proj.reason.see(reason_by_entry_change());
}
Interrupt::Compiled(artifact) => {
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &artifact.id);
proj.process_compile(artifact);
}
Interrupt::Settle(id) => {
self.remove_dedicates(&id);
}
Interrupt::ChangeTask(id, change) => {
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
proj.verse.increment_revision(|verse| {
if let Some(inputs) = change.inputs.clone() {
verse.set_inputs(inputs);
}
if let Some(entry) = change.entry.clone() {
let res = verse.mutate_entry(entry);
if let Err(err) = res {
log::error!("ProjectCompiler: change entry error: {err:?}");
}
}
});
// After incrementing the revision
if let Some(entry) = change.entry {
// todo: dedicate suspended
if entry.is_inactive() {
log::info!("ProjectCompiler: removing diag");
self.handler.status(
proj.verse.revision.get(),
&proj.id,
CompileReport::Suspend,
);
}
// Reset the watch state and document state.
proj.latest_doc = None;
proj.latest_success_doc = None;
}
proj.reason.see(reason_by_entry_change());
}
Interrupt::Font(fonts) => {
self.projects().for_each(|proj| {
let font_changed = proj.verse.increment_revision(|verse| {
verse.set_fonts(fonts.clone());
verse.font_changed()
});
if font_changed {
// todo: reason_by_font_change
proj.reason.see(reason_by_entry_change());
}
});
}
Interrupt::Memory(event) => {
log::debug!("ProjectCompiler: memory event incoming");
// Emulate memory changes.
let mut files = HashSet::new();
if matches!(event, MemoryEvent::Sync(..)) {
std::mem::swap(&mut files, &mut self.estimated_shadow_files);
}
let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
for path in &e.removes {
self.estimated_shadow_files.remove(path);
files.insert(Arc::clone(path));
}
for (path, _) in &e.inserts {
self.estimated_shadow_files.insert(Arc::clone(path));
files.remove(path);
}
// If there is no invalidation happening, apply memory changes directly.
if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
for (proj, event) in std::iter::once(&mut self.primary).zip(changes) {
let vfs_changed = proj.verse.increment_revision(|verse| {
Self::apply_memory_changes(&mut verse.vfs(), event.clone());
verse.vfs_changed()
});
if vfs_changed {
proj.reason.see(reason_by_mem());
}
}
return;
}
// Otherwise, send upstream update event.
// Also, record the logical tick when shadow is dirty.
self.dirty_shadow_logical_tick = self.logical_tick;
let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
invalidates: files.into_iter().collect(),
opaque: Box::new(TaggedMemoryEvent {
logical_tick: self.logical_tick,
event,
}),
});
let err = self.dep_tx.send(event);
log_send_error("dep_tx", err);
}
Interrupt::Fs(event) => {
log::debug!("ProjectCompiler: fs event incoming {event:?}");
// Apply file system changes.
let dirty_tick = &mut self.dirty_shadow_logical_tick;
let (changes, event) = event.split();
let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
for (proj, changes) in proj.zip(changes) {
let vfs_changed = proj.verse.increment_revision(|verse| {
{
let mut vfs = verse.vfs();
// Handle delayed upstream update event before applying file system
// changes
if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
.is_none()
{
log::warn!("ProjectCompiler: unknown upstream update event");
// Actual a delayed memory event.
proj.reason.see(reason_by_mem());
}
vfs.notify_fs_changes(changes);
}
verse.vfs_changed()
});
if vfs_changed {
proj.reason.see(reason_by_fs());
}
}
}
}
}
pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> ZResult<ProjectInsId> {
let id = ProjectInsId(group.into());
let verse = CompilerUniverse::<F>::new_raw(
entry,
Some(self.primary.verse.inputs().clone()),
self.primary.verse.vfs().fork(),
self.primary.verse.registry.clone(),
self.primary.verse.font_resolver.clone(),
);
let proj = Self::create_project(
id.clone(),
verse,
self.handler.clone(),
self.dep_tx.clone(),
self.primary.once_feature_set.as_ref().to_owned(),
);
self.remove_dedicates(&id);
self.dedicates.push(proj);
Ok(id)
}
fn remove_dedicates(&mut self, id: &ProjectInsId) {
let proj = self.dedicates.iter().position(|e| e.id == *id);
if let Some(idx) = proj {
let _proj = self.dedicates.remove(idx);
// todo: kill compilations
} else {
log::warn!("ProjectCompiler: settle project not found {id:?}");
}
}
}
pub struct ProjectState<F: CompilerFeat, Ext> {
pub id: ProjectInsId,
/// The extension
pub ext: Ext,
/// The underlying universe.
pub verse: CompilerUniverse<F>,
/// The reason to compile.
pub reason: CompileReasons,
/// The latest snapshot.
snapshot: Option<CompileSnapshot<F>>,
/// The latest compilation.
pub compilation: OnceLock<CompiledArtifact<F>>,
/// The compilation handle.
pub handler: Arc<dyn CompileHandler<F, Ext>>,
/// Channel for sending interrupts to the compiler actor.
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
/// The latest compiled document.
pub(crate) latest_doc: Option<Arc<TypstDocument>>,
/// The latest successly compiled document.
latest_success_doc: Option<Arc<TypstDocument>>,
/// feature set for compile_once mode.
once_feature_set: Arc<FeatureSet>,
/// Shared feature set for watch mode.
watch_feature_set: Arc<FeatureSet>,
committed_revision: usize,
}
impl<F: CompilerFeat, Ext: 'static> ProjectState<F, Ext> {
pub fn make_env(&self, feature_set: Arc<FeatureSet>) -> CompileEnv {
CompileEnv::default().configure_shared(feature_set)
}
pub fn snapshot(&mut self) -> CompileSnapshot<F> {
match self.snapshot.as_ref() {
Some(snap) if snap.world.revision() == self.verse.revision => snap.clone(),
_ => {
let snap = self.make_snapshot(false);
self.snapshot = Some(snap.clone());
snap
}
}
}
fn make_snapshot(&self, is_once: bool) -> CompileSnapshot<F> {
let world = self.verse.snapshot();
let env = self.make_env(if is_once {
self.once_feature_set.clone()
} else {
self.watch_feature_set.clone()
});
CompileSnapshot {
id: self.id.clone(),
world,
env: env.clone(),
signal: ExportSignal {
by_entry_update: self.reason.by_entry_update,
by_mem_events: self.reason.by_memory_events,
by_fs_events: self.reason.by_fs_events,
},
success_doc: self.latest_success_doc.clone(),
}
}
fn process_compile(&mut self, artifact: CompiledArtifact<F>) {
let world = &artifact.snap.world;
let compiled_revision = world.revision().get();
if self.committed_revision >= compiled_revision {
return;
}
// Update state.
let doc = artifact.doc.ok();
self.committed_revision = compiled_revision;
self.latest_doc.clone_from(&doc);
if doc.is_some() {
self.latest_success_doc.clone_from(&self.latest_doc);
}
// Notify the new file dependencies.
let mut deps = vec![];
world.iter_dependencies(&mut |dep| {
if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
deps.push(x.into())
}
});
let event = NotifyMessage::SyncDependency(deps);
let err = self.dep_tx.send(event);
log_send_error("dep_tx", err);
let mut world = artifact.snap.world;
let is_primary = self.id == ProjectInsId("primary".into());
// Trigger an evict task.
rayon::spawn(move || {
let evict_start = std::time::Instant::now();
if is_primary {
comemo::evict(10);
// Since all the projects share the same cache, we need to evict the cache
// on the primary instance for all the projects.
world.evict_source_cache(30);
}
world.evict_vfs(60);
let elapsed = evict_start.elapsed();
log::info!("ProjectCompiler: evict cache in {elapsed:?}");
});
}
#[must_use]
pub fn may_compile(
&mut self,
handler: &Arc<dyn CompileHandler<F, Ext>>,
) -> Option<impl FnOnce() -> CompiledArtifact<F>> {
if !self.reason.any() || self.verse.entry_state().is_inactive() {
return None;
}
let snap = self.snapshot();
self.reason = Default::default();
Some(Self::run_compile(handler.clone(), snap))
}
/// Compile the document once.
fn run_compile(
h: Arc<dyn CompileHandler<F, Ext>>,
snap: CompileSnapshot<F>,
) -> impl FnOnce() -> CompiledArtifact<F> {
let start = tinymist_std::time::now();
// todo unwrap main id
let id = snap.world.main_id().unwrap();
let revision = snap.world.revision().get();
h.status(
revision,
&snap.id,
CompileReport::Stage(id, "compiling", start),
);
move || {
let compiled = snap.compile();
let elapsed = start.elapsed().unwrap_or_default();
let rep = match &compiled.doc {
Ok(..) => CompileReport::CompileSuccess(id, compiled.warnings.clone(), elapsed),
Err(err) => CompileReport::CompileError(id, err.clone(), elapsed),
};
// todo: we need to check revision for really concurrent compilation
log_compile_report(&compiled.env, &rep);
h.notify_compile(&compiled, rep);
compiled
}
}
}
fn log_compile_report(env: &CompileEnv, rep: &CompileReport) {
if WITH_COMPILING_STATUS_FEATURE.retrieve(&env.features) {
log::info!("{}", rep.message());
}
}
#[inline]
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
.is_ok()
}

View file

@ -2,14 +2,16 @@
#![allow(missing_docs)]
mod lock;
pub use lock::*;
mod model;
pub use model::*;
mod args;
pub use args::*;
mod watch;
pub use watch::*;
pub mod world;
pub use world::*;
mod compiler;
pub mod font;
mod lock;
mod model;
mod watch;
pub mod world;
pub use args::*;
pub use compiler::*;
pub use lock::*;
pub use model::*;
pub use watch::*;
pub use world::*;

View file

@ -1,10 +1,11 @@
use std::{path::Path, sync::Arc};
use ecow::EcoVec;
use tinymist_std::path::unix_slash;
use tinymist_world::EntryReader;
use typst::{diag::EcoString, syntax::FileId};
use super::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath};
use crate::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath};
use crate::LspWorld;
/// Make a new project lock updater.
@ -81,7 +82,7 @@ impl ProjectLockUpdater {
self.updates.push(LockUpdate::Task(task));
}
pub fn update_materials(&mut self, doc_id: Id, ids: Vec<FileId>) {
pub fn update_materials(&mut self, doc_id: Id, ids: EcoVec<FileId>) {
let mut files = ids
.into_iter()
.map(ResourcePath::from_file_id)

View file

@ -25,6 +25,7 @@ pub use signature::*;
pub mod semantic_tokens;
pub use semantic_tokens::*;
use tinymist_world::vfs::WorkspaceResolver;
use tinymist_world::WorldDeps;
use typst::syntax::Source;
use typst::World;
mod post_tyck;
@ -39,7 +40,7 @@ mod prelude;
mod global;
pub use global::*;
use ecow::eco_format;
use ecow::{eco_format, EcoVec};
use lsp_types::Url;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Func, Value};
@ -71,6 +72,10 @@ pub trait LspWorldExt {
/// Resolve the uri for a file id.
fn uri_for_id(&self, fid: FileId) -> FileResult<Url>;
/// Get all depended file ids of a compilation, inclusively.
/// Note: must be called after compilation.
fn depended_files(&self) -> EcoVec<FileId>;
}
impl LspWorldExt for tinymist_project::LspWorld {
@ -96,6 +101,14 @@ impl LspWorldExt for tinymist_project::LspWorld {
log::info!("uri_for_id: {fid:?} -> {res:?}");
res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
}
fn depended_files(&self) -> EcoVec<FileId> {
let mut deps = EcoVec::new();
self.iter_dependencies(&mut |file_id| {
deps.push(file_id);
});
deps
}
}
#[cfg(test)]

View file

@ -12,7 +12,7 @@ use tinymist_project::LspWorld;
use tinymist_std::debug_loc::DataSource;
use tinymist_std::hash::{hash128, FxDashMap};
use tinymist_world::vfs::{PathResolution, WorkspaceResolver};
use tinymist_world::{EntryReader, WorldDeps, DETACHED_ENTRY};
use tinymist_world::{EntryReader, DETACHED_ENTRY};
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
use typst::engine::{Route, Sink, Traced};
use typst::eval::Eval;
@ -370,12 +370,7 @@ impl LocalContext {
/// Get all depended file ids of a compilation, inclusively.
/// Note: must be called after compilation.
pub fn depended_files(&self) -> EcoVec<TypstFileId> {
let mut deps = EcoVec::new();
self.world.iter_dependencies(&mut |file_id| {
deps.push(file_id);
});
deps
self.world.depended_files()
}
/// Get the world surface for Typst compiler.

View file

@ -205,6 +205,16 @@ impl<M: PathAccessModel + Clone + Sized> Vfs<M> {
access_model: self.access_model.clone(),
}
}
pub fn fork(&self) -> Self {
Self {
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"),
access_model: self.access_model.clone(),
}
}
}
impl<M: PathAccessModel + Sized> Vfs<M> {
@ -245,8 +255,8 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
/// Reset all state.
pub fn reset_all(&mut self) {
self.reset_access_model();
self.reset_cache();
self.take_state();
self.reset_mapping();
self.take_source_cache();
}
/// Reset access model.
@ -255,7 +265,7 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
}
/// Reset all possible caches.
pub fn reset_cache(&mut self) {
pub fn reset_mapping(&mut self) {
self.revise().reset_cache();
}
@ -271,10 +281,14 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
}
}
pub fn take_state(&mut self) -> SourceCache {
pub fn take_source_cache(&mut self) -> SourceCache {
std::mem::take(&mut self.source_cache)
}
pub fn clone_source_cache(&self) -> SourceCache {
self.source_cache.clone()
}
/// Resolve the real path for a file id.
pub fn file_path(&self, id: TypstFileId) -> Result<PathResolution, FileError> {
self.access_model.inner.resolver.path_for_id(id)

View file

@ -38,7 +38,6 @@ tar.workspace = true
tinymist-std.workspace = true
tinymist-vfs.workspace = true
typst.workspace = true
toml.workspace = true
typst-assets.workspace = true
wasm-bindgen = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = ["console"] }

View file

@ -147,6 +147,10 @@ impl<F: CompilerFeat> CompilerUniverse<F> {
self.mutate_entry_(state).map(|_| ())?;
Ok(())
}
pub fn vfs(&self) -> &Vfs<F::AccessModel> {
&self.vfs
}
}
impl<F: CompilerFeat> CompilerUniverse<F> {
@ -432,7 +436,11 @@ impl<F: CompilerFeat> CompilerWorld<F> {
}
pub fn take_cache(&mut self) -> SourceCache {
self.vfs.take_state()
self.vfs.take_source_cache()
}
pub fn clone_cache(&mut self) -> SourceCache {
self.vfs.clone_source_cache()
}
pub fn take_db(&mut self) -> SourceDb {
@ -486,6 +494,16 @@ impl<F: CompilerFeat> CompilerWorld<F> {
pub fn revision(&self) -> NonZeroUsize {
self.revision
}
pub fn evict_vfs(&mut self, threshold: usize) {
self.vfs.evict(threshold);
}
pub fn evict_source_cache(&mut self, threshold: usize) {
self.vfs
.clone_source_cache()
.evict(self.vfs.revision(), threshold);
}
}
impl<F: CompilerFeat> ShadowApi for CompilerWorld<F> {

View file

@ -3,184 +3,3 @@
pub mod editor;
#[cfg(feature = "preview")]
pub mod preview;
pub mod typ_client;
pub mod typ_server;
use std::sync::Arc;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use crate::world::EntryState;
use reflexo::ImmutPath;
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
use tinymist_query::{ExportKind, LocalContext, VersionedDocument};
use tinymist_render::PeriscopeRenderer;
use tokio::sync::mpsc;
use typst::layout::Position;
use crate::{
project::{ImmutDict, LspUniverseBuilder},
task::{ExportConfig, ExportTask, ExportUserConfig},
LanguageState,
};
use typ_client::{CompileClientActor, CompileHandler};
use typ_server::{CompileServerActor, CompileServerOpts};
impl LanguageState {
/// Restart the primary server.
pub fn restart_primary(&mut self) {
let entry = self.entry_resolver().resolve_default();
self.restart_server("primary", entry);
}
/// Restart the server with the given group.
pub fn restart_dedicate(&mut self, dedicate: &str, entry: Option<ImmutPath>) {
self.restart_server(dedicate, entry);
}
/// Restart the server with the given group.
fn restart_server(&mut self, group: &str, entry: Option<ImmutPath>) {
let server = self.server(
group.to_owned(),
self.entry_resolver().resolve(entry),
self.compile_config().determine_inputs(),
self.vfs_snapshot(),
);
let prev = if group == "primary" {
self.primary.replace(server)
} else {
let cell = self
.dedicates
.iter_mut()
.find(|dedicate| dedicate.handle.diag_group == group);
if let Some(dedicate) = cell {
Some(std::mem::replace(dedicate, server))
} else {
self.dedicates.push(server);
None
}
};
if let Some(mut prev) = prev {
self.client.handle.spawn(async move { prev.settle().await });
}
}
/// Create a new server for the given group.
pub fn server(
&self,
editor_group: String,
entry: EntryState,
inputs: ImmutDict,
snapshot: FileChangeSet,
) -> CompileClientActor {
let (intr_tx, intr_rx) = mpsc::unbounded_channel();
// Run Export actors before preparing cluster to avoid loss of events
let export = ExportTask::new(ExportConfig {
group: editor_group.clone(),
editor_tx: Some(self.editor_tx.clone()),
config: ExportUserConfig {
output: self.compile_config().output_path.clone(),
mode: self.compile_config().export_pdf,
},
kind: ExportKind::Pdf {
creation_timestamp: self.config.compile.determine_creation_timestamp(),
},
count_words: self.config.compile.notify_status,
});
log::info!(
"TypstActor: creating server for {editor_group}, entry: {entry:?}, inputs: {inputs:?}"
);
// Create the compile handler for client consuming results.
let const_config = self.const_config();
let periscope_args = self.compile_config().periscope_args.clone();
let handle = Arc::new(CompileHandler {
#[cfg(feature = "preview")]
inner: std::sync::Arc::new(parking_lot::RwLock::new(None)),
diag_group: editor_group.clone(),
intr_tx: intr_tx.clone(),
export: export.clone(),
editor_tx: self.editor_tx.clone(),
stats: Default::default(),
analysis: Arc::new(Analysis {
position_encoding: const_config.position_encoding,
allow_overlapping_token: const_config.tokens_overlapping_token_support,
allow_multiline_token: const_config.tokens_multiline_token_support,
remove_html: !self.config.support_html_in_markdown,
completion_feat: self.config.completion.clone(),
color_theme: match self.compile_config().color_theme.as_deref() {
Some("dark") => tinymist_query::ColorTheme::Dark,
_ => tinymist_query::ColorTheme::Light,
},
periscope: periscope_args.map(|args| {
let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args));
Arc::new(r) as Arc<dyn PeriscopeProvider + Send + Sync>
}),
tokens_caches: Arc::default(),
workers: Default::default(),
caches: Default::default(),
analysis_rev_cache: Arc::default(),
stats: Arc::default(),
}),
notified_revision: parking_lot::Mutex::new(0),
});
let font_resolver = self.compile_config().determine_fonts();
let entry_ = entry.clone();
let compile_handle = handle.clone();
let cache = self.cache.clone();
let cert_path = self.compile_config().determine_certification_path();
let package = self.compile_config().determine_package_opts();
self.client.handle.spawn_blocking(move || {
// Create the world
let font_resolver = font_resolver.wait().clone();
let package_registry =
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
let verse =
LspUniverseBuilder::build(entry_.clone(), inputs, font_resolver, package_registry)
.expect("incorrect options");
// Create the actor
let server = CompileServerActor::new_with(
verse,
intr_tx,
intr_rx,
CompileServerOpts {
compile_handle,
cache,
..Default::default()
},
)
.with_watch(true);
tokio::spawn(server.run());
});
// Create the client
let config = self.compile_config().clone();
let client = CompileClientActor::new(handle, config, entry);
// We do send memory changes instead of initializing compiler with them.
// This is because there are state recorded inside of the compiler actor, and we
// must update them.
client.add_memory_changes(MemoryEvent::Update(snapshot));
client
}
}
struct TypstPeriscopeProvider(PeriscopeRenderer);
impl PeriscopeProvider for TypstPeriscopeProvider {
/// Resolve periscope image at the given position.
fn periscope_at(
&self,
ctx: &mut LocalContext,
doc: VersionedDocument,
pos: Position,
) -> Option<String> {
self.0.render_marked(ctx, doc, pos)
}
}

View file

@ -7,8 +7,10 @@ use sync_lsp::{internal_error, LspClient, LspResult};
use tokio::sync::{mpsc, oneshot};
use typst_preview::{ControlPlaneMessage, Previewer};
use super::typ_client::CompileHandler;
use crate::tool::preview::HttpServer;
use crate::{
project::LspPreviewState,
tool::preview::{HttpServer, PreviewProjectHandler},
};
pub struct PreviewTab {
/// Task ID
@ -20,7 +22,7 @@ pub struct PreviewTab {
/// Control plane message sender
pub ctl_tx: mpsc::UnboundedSender<ControlPlaneMessage>,
/// Compile handler
pub compile_handler: Arc<CompileHandler>,
pub compile_handler: Arc<PreviewProjectHandler>,
/// Whether this tab is primary
pub is_primary: bool,
}
@ -35,6 +37,8 @@ pub struct PreviewActor {
pub client: LspClient,
pub tabs: HashMap<String, PreviewTab>,
pub preview_rx: mpsc::UnboundedReceiver<PreviewRequest>,
/// the watchers for the preview
pub(crate) watchers: LspPreviewState,
}
impl PreviewActor {
@ -52,7 +56,7 @@ impl PreviewActor {
};
// Unregister preview early
let unregistered = tab.compile_handler.unregister_preview(&task_id);
let unregistered = self.watchers.unregister(&tab.compile_handler.project_id);
if !unregistered {
log::warn!("PreviewTask({task_id}): failed to unregister preview");
}

View file

@ -1,539 +0,0 @@
//! The actor that runs compilations.
//!
//! ```ascii
//! ┌────────────────────────────────┐
//! │ main::compile_actor (client) │
//! └─────┬────────────────────▲─────┘
//! │ │
//! ┌─────▼────────────────────┴─────┐ ┌────────────┐
//! │compiler::compile_actor (server)│◄───────►│notify_actor│
//! └─────┬────────────────────▲─────┘ └────────────┘
//! │ │
//! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐
//! │ compiler::compile_handler ├────────►│ rest actors│
//! └────────────────────────────────┘ └────────────┘
//! ```
//!
//! We use typst by creating a
//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`] and
//! running compiler with callbacking [`CompileHandler`] incrementally. An
//! additional [`CompileClientActor`] is also created to control the
//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`].
//!
//! The [`CompileHandler`] will push information to other actors.
use std::{collections::HashMap, ops::Deref, sync::Arc};
use anyhow::bail;
use log::{error, info, trace};
use reflexo::path::unix_slash;
use reflexo_typst::{error::prelude::*, typst::prelude::*, CompileReport, Error, ImmutPath};
use sync_lsp::just_future;
use tinymist_query::{
analysis::{Analysis, AnalysisRevLock, LocalContextGuard},
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, EntryResolver, OnExportRequest,
SemanticRequest, ServerInfoResponse, StatefulRequest, VersionedDocument,
};
use tokio::sync::{mpsc, oneshot};
use typst::{diag::SourceDiagnostic, World};
use super::{
editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum},
typ_server::{
CompilationHandle, CompileSnapshot, CompiledArtifact, Interrupt, SucceededArtifact,
},
};
use crate::world::{vfs::notify::MemoryEvent, EntryReader, EntryState, TaskInputs};
use crate::{
stats::{CompilerQueryStats, QueryStatGuard},
task::{ExportTask, ExportUserConfig},
world::{LspCompilerFeat, LspWorld},
CompileConfig, QueryFuture,
};
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
pub struct CompileHandler {
pub(crate) diag_group: String,
pub(crate) analysis: Arc<Analysis>,
pub(crate) stats: CompilerQueryStats,
#[cfg(feature = "preview")]
pub(crate) inner: Arc<parking_lot::RwLock<Option<Arc<typst_preview::CompileWatcher>>>>,
pub(crate) intr_tx: mpsc::UnboundedSender<Interrupt<LspCompilerFeat>>,
pub(crate) export: ExportTask,
pub(crate) editor_tx: EditorSender,
pub(crate) notified_revision: parking_lot::Mutex<usize>,
}
impl CompileHandler {
/// Snapshot the compiler thread for tasks
pub fn snapshot(&self) -> ZResult<WorldSnapFut> {
let (tx, rx) = oneshot::channel();
self.intr_tx
.send(Interrupt::SnapshotRead(tx))
.map_err(map_string_err("failed to send snapshot request"))?;
Ok(WorldSnapFut { rx })
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&self, q: Option<&CompilerQueryRequest>) -> ZResult<QuerySnapFut> {
let fut = self.snapshot()?;
let analysis = self.analysis.clone();
let rev_lock = analysis.lock_revision(q);
Ok(QuerySnapFut {
fut,
analysis,
rev_lock,
})
}
/// Get latest artifact the compiler thread for tasks
pub fn artifact(&self) -> ZResult<ArtifactSnap> {
let (tx, rx) = oneshot::channel();
self.intr_tx
.send(Interrupt::CurrentRead(tx))
.map_err(map_string_err("failed to send snapshot request"))?;
Ok(ArtifactSnap { rx })
}
pub fn flush_compile(&self) {
// todo: better way to flush compile
let _ = self.intr_tx.send(Interrupt::Compile);
}
pub fn add_memory_changes(&self, event: MemoryEvent) {
let _ = self.intr_tx.send(Interrupt::Memory(event));
}
pub fn change_task(&self, task_inputs: TaskInputs) {
let _ = self.intr_tx.send(Interrupt::ChangeTask(task_inputs));
}
pub async fn settle(&self) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel();
let _ = self.intr_tx.send(Interrupt::Settle(tx));
rx.await?;
Ok(())
}
fn push_diagnostics(&self, revision: usize, diagnostics: Option<DiagnosticsMap>) {
let dv = DocVersion {
group: self.diag_group.clone(),
revision,
};
let res = self.editor_tx.send(EditorRequest::Diag(dv, diagnostics));
if let Err(err) = res {
error!("failed to send diagnostics: {err:#}");
}
}
fn notify_diagnostics(
&self,
world: &LspWorld,
errors: EcoVec<SourceDiagnostic>,
warnings: EcoVec<SourceDiagnostic>,
) {
let revision = world.revision().get();
trace!("notify diagnostics({revision}): {errors:#?} {warnings:#?}");
let diagnostics = tinymist_query::convert_diagnostics(
world,
errors.iter().chain(warnings.iter()),
self.analysis.position_encoding,
);
let entry = world.entry_state();
// todo: better way to remove diagnostics
// todo: check all errors in this file
let detached = entry.is_inactive();
let valid = !detached;
self.push_diagnostics(revision, valid.then_some(diagnostics));
}
// todo: multiple preview support
#[cfg(feature = "preview")]
#[must_use]
pub fn register_preview(&self, handle: &Arc<typst_preview::CompileWatcher>) -> bool {
let mut p = self.inner.write();
if p.as_ref().is_some() {
return false;
}
*p = Some(handle.clone());
true
}
#[cfg(feature = "preview")]
#[must_use]
pub fn unregister_preview(&self, task_id: &str) -> bool {
let mut p = self.inner.write();
if p.as_ref().is_some_and(|p| p.task_id() == task_id) {
*p = None;
return true;
}
false
}
}
impl CompilationHandle<LspCompilerFeat> for CompileHandler {
fn status(&self, revision: usize, rep: CompileReport) {
// todo: seems to duplicate with CompileStatus
let status = match rep {
CompileReport::Suspend => {
self.push_diagnostics(revision, None);
TinymistCompileStatusEnum::CompileSuccess
}
CompileReport::Stage(_, _, _) => TinymistCompileStatusEnum::Compiling,
CompileReport::CompileSuccess(_, _, _) => TinymistCompileStatusEnum::CompileSuccess,
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
TinymistCompileStatusEnum::CompileError
}
};
let this = &self;
this.editor_tx
.send(EditorRequest::Status(CompileStatus {
group: this.diag_group.clone(),
path: rep
.compiling_id()
.map(|s| unix_slash(s.vpath().as_rooted_path()))
.unwrap_or_default(),
status,
}))
.unwrap();
#[cfg(feature = "preview")]
if let Some(inner) = this.inner.read().as_ref() {
use typst_preview::CompileStatus;
let status = match rep {
CompileReport::Suspend => CompileStatus::CompileSuccess,
CompileReport::Stage(_, _, _) => CompileStatus::Compiling,
CompileReport::CompileSuccess(_, _, _) => CompileStatus::CompileSuccess,
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
CompileStatus::CompileError
}
};
inner.status(status);
}
}
fn notify_compile(&self, snap: &CompiledArtifact<LspCompilerFeat>, rep: CompileReport) {
// todo: we need to manage the revision for fn status() as well
{
let mut n_rev = self.notified_revision.lock();
if *n_rev >= snap.world.revision().get() {
log::info!(
"TypstActor: already notified for revision {} <= {n_rev}",
snap.world.revision(),
);
return;
}
*n_rev = snap.world.revision().get();
}
self.notify_diagnostics(
&snap.world,
snap.doc.clone().err().unwrap_or_default(),
snap.warnings.clone(),
);
self.export.signal(snap, snap.signal);
self.editor_tx
.send(EditorRequest::Status(CompileStatus {
group: self.diag_group.clone(),
path: rep
.compiling_id()
.map(|s| unix_slash(s.vpath().as_rooted_path()))
.unwrap_or_default(),
status: if snap.doc.is_ok() {
TinymistCompileStatusEnum::CompileSuccess
} else {
TinymistCompileStatusEnum::CompileError
},
}))
.unwrap();
#[cfg(feature = "preview")]
if let Some(inner) = self.inner.read().as_ref() {
let snap = snap.clone();
inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { snap }));
}
}
}
pub struct CompileClientActor {
pub handle: Arc<CompileHandler>,
pub config: CompileConfig,
entry: EntryState,
}
impl CompileClientActor {
pub(crate) fn new(
handle: Arc<CompileHandler>,
config: CompileConfig,
entry: EntryState,
) -> Self {
Self {
handle,
config,
entry,
}
}
/// Snapshot the compiler thread for tasks
pub fn snapshot(&self) -> ZResult<WorldSnapFut> {
self.handle.clone().snapshot()
}
/// Get the entry resolver.
pub fn entry_resolver(&self) -> &EntryResolver {
&self.config.entry_resolver
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&self) -> ZResult<QuerySnapFut> {
self.handle.clone().query_snapshot(None)
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot_with_stat(&self, q: &CompilerQueryRequest) -> ZResult<QuerySnapWithStat> {
let name: &'static str = q.into();
let path = q.associated_path();
let stat = self.handle.stats.query_stat(path, name);
let fut = self.handle.clone().query_snapshot(Some(q))?;
Ok(QuerySnapWithStat { fut, stat })
}
pub fn add_memory_changes(&self, event: MemoryEvent) {
self.handle.add_memory_changes(event);
}
pub fn change_task(&self, task_inputs: TaskInputs) {
self.handle.change_task(task_inputs);
}
pub fn sync_config(&mut self, config: CompileConfig) {
self.config = config;
}
pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
self.handle.export.change_config(config);
}
pub fn on_export(&self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, kind, open } = req;
let snap = self.snapshot()?;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let export = self.handle.export.factory.oneshot(snap, Some(entry), kind);
just_future(async move {
let res = export.await?;
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
// Also see https://github.com/Byron/open-rs/issues/105
#[cfg(not(target_os = "windows"))]
let do_open = ::open::that_detached;
#[cfg(target_os = "windows")]
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
::open::with_detached(path, "explorer")
}
if let Some(Some(path)) = open.then_some(res.as_ref()) {
log::info!("open with system default apps: {path:?}");
if let Err(e) = do_open(path) {
log::error!("failed to open with system default apps: {e}");
};
}
log::info!("CompileActor: on export end: {path:?} as {res:?}");
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
})
}
}
impl CompileClientActor {
pub async fn settle(&mut self) {
let _ = self.change_entry(None);
info!("TypstActor({}): settle requested", self.handle.diag_group);
match self.handle.settle().await {
Ok(()) => info!("TypstActor({}): settled", self.handle.diag_group),
Err(err) => error!(
"TypstActor({}): failed to settle: {err:#}",
self.handle.diag_group
),
}
}
pub fn change_entry(&mut self, path: Option<ImmutPath>) -> Result<bool, Error> {
if path
.as_deref()
.is_some_and(|p| !p.is_absolute() && !p.starts_with("/untitled"))
{
return Err(error_once!("entry file must be absolute", path: path.unwrap().display()));
}
let next_entry = self.entry_resolver().resolve(path);
if next_entry == self.entry {
return Ok(false);
}
let diag_group = &self.handle.diag_group;
info!("the entry file of TypstActor({diag_group}) is changing to {next_entry:?}");
self.change_task(TaskInputs {
entry: Some(next_entry.clone()),
..Default::default()
});
self.entry = next_entry;
Ok(true)
}
pub fn clear_cache(&mut self) {
self.handle.analysis.clear_cache();
}
pub fn collect_server_info(&self) -> QueryFuture {
let dg = self.handle.diag_group.clone();
let api_stats = self.handle.stats.report();
let query_stats = self.handle.analysis.report_query_stats();
let alloc_stats = self.handle.analysis.report_alloc_stats();
let snap = self.snapshot()?;
just_future(async move {
let snap = snap.receive().await?;
let w = &snap.world;
let info = ServerInfoResponse {
root: w.entry_state().root().map(|e| e.as_ref().to_owned()),
font_paths: w.font_resolver.font_paths().to_owned(),
inputs: w.inputs().as_ref().deref().clone(),
stats: HashMap::from_iter([
("api".to_owned(), api_stats),
("query".to_owned(), query_stats),
("alloc".to_owned(), alloc_stats),
]),
};
let info = Some(HashMap::from_iter([(dg, info)]));
Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info))
})
}
}
pub struct QuerySnapWithStat {
pub fut: QuerySnapFut,
pub(crate) stat: QueryStatGuard,
}
pub struct WorldSnapFut {
rx: oneshot::Receiver<CompileSnapshot<LspCompilerFeat>>,
}
impl WorldSnapFut {
/// wait for the snapshot to be ready
pub async fn receive(self) -> ZResult<CompileSnapshot<LspCompilerFeat>> {
self.rx
.await
.map_err(map_string_err("failed to get snapshot"))
}
}
pub struct QuerySnapFut {
fut: WorldSnapFut,
analysis: Arc<Analysis>,
rev_lock: AnalysisRevLock,
}
impl QuerySnapFut {
/// wait for the snapshot to be ready
pub async fn receive(self) -> ZResult<QuerySnap> {
let snap = self.fut.receive().await?;
Ok(QuerySnap {
snap,
analysis: self.analysis,
rev_lock: self.rev_lock,
})
}
}
pub struct QuerySnap {
pub snap: CompileSnapshot<LspCompilerFeat>,
analysis: Arc<Analysis>,
rev_lock: AnalysisRevLock,
}
impl std::ops::Deref for QuerySnap {
type Target = CompileSnapshot<LspCompilerFeat>;
fn deref(&self) -> &Self::Target {
&self.snap
}
}
impl QuerySnap {
pub fn task(mut self, inputs: TaskInputs) -> Self {
self.snap = self.snap.task(inputs);
self
}
pub fn run_stateful<T: StatefulRequest>(
self,
query: T,
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
) -> anyhow::Result<CompilerQueryResponse> {
let doc = self.snap.success_doc.as_ref().map(|doc| VersionedDocument {
version: self.world.revision().get(),
document: doc.clone(),
});
self.run_analysis(|ctx| query.request(ctx, doc))
.map(wrapper)
}
pub fn run_semantic<T: SemanticRequest>(
self,
query: T,
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
) -> anyhow::Result<CompilerQueryResponse> {
self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
}
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");
bail!("main file is not set");
};
world.source(main).map_err(|err| {
info!("TypstActor: failed to prepare main file: {err:?}");
anyhow::anyhow!("failed to get source: {err}")
})?;
let mut analysis = self.analysis.snapshot_(world, self.rev_lock);
Ok(f(&mut analysis))
}
}
pub struct ArtifactSnap {
rx: oneshot::Receiver<SucceededArtifact<LspCompilerFeat>>,
}
impl ArtifactSnap {
/// Get latest artifact the compiler thread for tasks
pub async fn receive(self) -> ZResult<SucceededArtifact<LspCompilerFeat>> {
self.rx
.await
.map_err(map_string_err("failed to get snapshot"))
}
}

View file

@ -1,747 +0,0 @@
//! The [`CompileServerActor`] implementation borrowed from typst.ts.
//!
//! Please check `tinymist::actor::typ_client` for architecture details.
use std::{
collections::HashSet,
path::Path,
sync::{Arc, OnceLock},
};
use reflexo_typst::{
features::WITH_COMPILING_STATUS_FEATURE, typst::prelude::EcoVec, CompileEnv, CompileReport,
Compiler, ConsoleDiagReporter, FeatureSet, GenericExporter, TypstDocument,
};
use tinymist_project::watch_deps;
use tinymist_project::{vfs::FsProvider, RevisingUniverse};
use tokio::sync::{mpsc, oneshot};
use typst::diag::{SourceDiagnostic, SourceResult};
use crate::task::CacheTask;
use crate::world::base::{
// features::{FeatureSet, WITH_COMPILING_STATUS_FEATURE},
// typst::prelude::EcoVec,
vfs::notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
CompilerFeat,
CompilerUniverse,
CompilerWorld,
EntryReader,
TaskInputs,
WorldDeps,
};
/// A signal that possibly triggers an export.
///
/// Whether to export depends on the current state of the document and the user
/// settings.
#[derive(Debug, Clone, Copy)]
pub struct ExportSignal {
/// Whether the revision is annotated by memory events.
pub by_mem_events: bool,
/// Whether the revision is annotated by file system events.
pub by_fs_events: bool,
/// Whether the revision is annotated by entry update.
pub by_entry_update: bool,
}
pub struct CompileSnapshot<F: CompilerFeat> {
/// The export signal for the document.
pub signal: ExportSignal,
/// Using env
pub env: CompileEnv,
/// Using world
pub world: CompilerWorld<F>,
/// The last successfully compiled document.
pub success_doc: Option<Arc<TypstDocument>>,
}
impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
pub fn task(mut self, inputs: TaskInputs) -> Self {
'check_changed: {
if let Some(entry) = &inputs.entry {
if *entry != self.world.entry_state() {
break 'check_changed;
}
}
if let Some(inputs) = &inputs.inputs {
if inputs.clone() != self.world.inputs() {
break 'check_changed;
}
}
return self;
};
self.world = self.world.task(inputs);
self
}
pub async fn compile(self) -> CompiledArtifact<F> {
let mut snap = self;
let warned = std::marker::PhantomData.compile(&snap.world, &mut snap.env);
let (doc, warnings) = match warned {
Ok(doc) => (Ok(doc.output), doc.warnings),
Err(err) => (Err(err), EcoVec::default()),
};
CompiledArtifact {
snap,
doc,
warnings,
}
}
}
impl<F: CompilerFeat> Clone for CompileSnapshot<F> {
fn clone(&self) -> Self {
Self {
signal: self.signal,
env: self.env.clone(),
world: self.world.clone(),
success_doc: self.success_doc.clone(),
}
}
}
pub struct CompiledArtifact<F: CompilerFeat> {
/// The used snapshot.
pub snap: CompileSnapshot<F>,
/// The diagnostics of the document.
pub warnings: EcoVec<SourceDiagnostic>,
/// The compiled document.
pub doc: SourceResult<Arc<TypstDocument>>,
}
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
type Target = CompileSnapshot<F>;
fn deref(&self) -> &Self::Target {
&self.snap
}
}
impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
fn clone(&self) -> Self {
Self {
snap: self.snap.clone(),
doc: self.doc.clone(),
warnings: self.warnings.clone(),
}
}
}
impl<F: CompilerFeat> CompiledArtifact<F> {
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
self.doc
.as_ref()
.ok()
.cloned()
.or_else(|| self.snap.success_doc.clone())
}
}
pub trait CompilationHandle<F: CompilerFeat>: Send + Sync + 'static {
fn status(&self, revision: usize, rep: CompileReport);
fn notify_compile(&self, res: &CompiledArtifact<F>, rep: CompileReport);
}
impl<F: CompilerFeat + Send + Sync + 'static> CompilationHandle<F>
for std::marker::PhantomData<fn(F)>
{
fn status(&self, _revision: usize, _: CompileReport) {}
fn notify_compile(&self, _: &CompiledArtifact<F>, _: CompileReport) {}
}
pub enum SucceededArtifact<F: CompilerFeat> {
Compiled(CompiledArtifact<F>),
Suspend(CompileSnapshot<F>),
}
impl<F: CompilerFeat> SucceededArtifact<F> {
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
match self {
SucceededArtifact::Compiled(artifact) => artifact.success_doc(),
SucceededArtifact::Suspend(snapshot) => snapshot.success_doc.clone(),
}
}
pub fn world(&self) -> &CompilerWorld<F> {
match self {
SucceededArtifact::Compiled(artifact) => &artifact.world,
SucceededArtifact::Suspend(snapshot) => &snapshot.world,
}
}
}
pub enum Interrupt<F: CompilerFeat> {
/// Compile anyway.
Compile,
/// Compiled from computing thread.
Compiled(CompiledArtifact<F>),
/// Change the watching entry.
ChangeTask(TaskInputs),
/// Request compiler to respond a snapshot without needing to wait latest
/// compilation.
SnapshotRead(oneshot::Sender<CompileSnapshot<F>>),
/// Request compiler to respond a snapshot with at least a compilation
/// happens on or after current revision.
CurrentRead(oneshot::Sender<SucceededArtifact<F>>),
/// Memory file changes.
Memory(MemoryEvent),
/// File system event.
Fs(FilesystemEvent),
/// Request compiler to stop.
Settle(oneshot::Sender<()>),
}
/// Responses from the compiler actor.
enum CompilerResponse {
/// Response to the file watcher
Notify(NotifyMessage),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct CompileReasons {
/// The snapshot is taken by the memory editing events.
by_memory_events: bool,
/// The snapshot is taken by the file system events.
by_fs_events: bool,
/// The snapshot is taken by the entry change.
by_entry_update: bool,
}
impl CompileReasons {
fn see(&mut self, reason: CompileReasons) {
self.by_memory_events |= reason.by_memory_events;
self.by_fs_events |= reason.by_fs_events;
self.by_entry_update |= reason.by_entry_update;
}
fn any(&self) -> bool {
self.by_memory_events || self.by_fs_events || self.by_entry_update
}
}
fn no_reason() -> CompileReasons {
CompileReasons::default()
}
fn reason_by_mem() -> CompileReasons {
CompileReasons {
by_memory_events: true,
..CompileReasons::default()
}
}
fn reason_by_fs() -> CompileReasons {
CompileReasons {
by_fs_events: true,
..CompileReasons::default()
}
}
fn reason_by_entry_change() -> CompileReasons {
CompileReasons {
by_entry_update: true,
..CompileReasons::default()
}
}
/// A tagged memory event with logical tick.
struct TaggedMemoryEvent {
/// The logical tick when the event is received.
logical_tick: usize,
/// The memory event happened.
event: MemoryEvent,
}
pub struct CompileServerOpts<F: CompilerFeat> {
pub compile_handle: Arc<dyn CompilationHandle<F>>,
pub feature_set: FeatureSet,
pub cache: CacheTask,
}
impl<F: CompilerFeat + Send + Sync + 'static> Default for CompileServerOpts<F> {
fn default() -> Self {
Self {
compile_handle: Arc::new(std::marker::PhantomData),
feature_set: Default::default(),
cache: Default::default(),
}
}
}
/// The compiler actor.
pub struct CompileServerActor<F: CompilerFeat> {
/// The underlying universe.
pub verse: CompilerUniverse<F>,
/// The compilation handle.
pub compile_handle: Arc<dyn CompilationHandle<F>>,
/// Whether to enable file system watching.
pub enable_watch: bool,
/// The current logical tick.
logical_tick: usize,
/// Last logical tick when invalidation is caused by shadow update.
dirty_shadow_logical_tick: usize,
/// Estimated latest set of shadow files.
estimated_shadow_files: HashSet<Arc<Path>>,
/// The latest compiled document.
pub(crate) latest_doc: Option<Arc<TypstDocument>>,
/// The latest successly compiled document.
latest_success_doc: Option<Arc<TypstDocument>>,
/// feature set for compile_once mode.
once_feature_set: Arc<FeatureSet>,
/// Shared feature set for watch mode.
watch_feature_set: Arc<FeatureSet>,
/// Channel for sending interrupts to the compiler actor.
intr_tx: mpsc::UnboundedSender<Interrupt<F>>,
/// Channel for receiving interrupts from the compiler actor.
intr_rx: mpsc::UnboundedReceiver<Interrupt<F>>,
/// Shared cache evict task.
cache: CacheTask,
watch_snap: OnceLock<CompileSnapshot<F>>,
suspended: bool,
compiling: bool,
suspended_reason: CompileReasons,
committed_revision: usize,
}
impl<F: CompilerFeat + Send + Sync + 'static> CompileServerActor<F> {
/// Create a new compiler actor with options
pub fn new_with(
verse: CompilerUniverse<F>,
intr_tx: mpsc::UnboundedSender<Interrupt<F>>,
intr_rx: mpsc::UnboundedReceiver<Interrupt<F>>,
CompileServerOpts {
compile_handle,
feature_set,
cache: cache_evict,
}: CompileServerOpts<F>,
) -> Self {
let entry = verse.entry_state();
Self {
verse,
logical_tick: 1,
compile_handle,
enable_watch: false,
dirty_shadow_logical_tick: 0,
estimated_shadow_files: Default::default(),
latest_doc: None,
latest_success_doc: None,
once_feature_set: Arc::new(feature_set.clone()),
watch_feature_set: Arc::new(
feature_set.configure(&WITH_COMPILING_STATUS_FEATURE, true),
),
intr_tx,
intr_rx,
cache: cache_evict,
watch_snap: OnceLock::new(),
suspended: entry.is_inactive(),
compiling: false,
suspended_reason: no_reason(),
committed_revision: 0,
}
}
pub fn with_watch(mut self, watch: bool) -> Self {
self.enable_watch = watch;
self
}
fn make_env(&self, feature_set: Arc<FeatureSet>) -> CompileEnv {
CompileEnv::default().configure_shared(feature_set)
}
/// Launches the compiler actor.
pub async fn run(mut self) -> bool {
if !self.enable_watch {
let artifact = self.compile_once().await;
return artifact.doc.is_ok();
}
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
let mut curr_reads = vec![];
log::debug!("CompileServerActor: initialized");
// Trigger the first compilation (if active)
self.run_compile(reason_by_entry_change(), &mut curr_reads, false)
.await;
// Spawn file system watcher.
let fs_tx = self.intr_tx.clone();
tokio::spawn(watch_deps(dep_rx, move |event| {
log_send_error("fs_event", fs_tx.send(Interrupt::Fs(event)));
}));
'event_loop: while let Some(mut event) = self.intr_rx.recv().await {
let mut comp_reason = no_reason();
'accumulate: loop {
// Warp the logical clock by one.
self.logical_tick += 1;
// If settle, stop the actor.
if let Interrupt::Settle(e) = event {
log::info!("CompileServerActor: requested stop");
e.send(()).ok();
break 'event_loop;
}
if let Interrupt::CurrentRead(event) = event {
curr_reads.push(event);
} else {
comp_reason.see(self.process(event, |res: CompilerResponse| match res {
CompilerResponse::Notify(msg) => {
log_send_error("compile_deps", dep_tx.send(msg));
}
}));
}
// Try to accumulate more events.
match self.intr_rx.try_recv() {
Ok(new_event) => event = new_event,
_ => break 'accumulate,
}
}
// Either we have a reason to compile or we have events that want to have any
// compilation.
if comp_reason.any() || !curr_reads.is_empty() {
self.run_compile(comp_reason, &mut curr_reads, false).await;
}
}
log_send_error("settle_notify", dep_tx.send(NotifyMessage::Settle));
log::info!("CompileServerActor: exited");
true
}
fn snapshot(&self, is_once: bool, reason: CompileReasons) -> CompileSnapshot<F> {
let world = self.verse.snapshot();
let env = self.make_env(if is_once {
self.once_feature_set.clone()
} else {
self.watch_feature_set.clone()
});
CompileSnapshot {
world,
env: env.clone(),
signal: ExportSignal {
by_entry_update: reason.by_entry_update,
by_mem_events: reason.by_memory_events,
by_fs_events: reason.by_fs_events,
},
success_doc: self.latest_success_doc.clone(),
}
}
/// Compile the document once.
pub async fn compile_once(&mut self) -> CompiledArtifact<F> {
self.run_compile(reason_by_entry_change(), &mut vec![], true)
.await
.expect("is_once is set")
}
/// Compile the document once.
async fn run_compile(
&mut self,
reason: CompileReasons,
curr_reads: &mut Vec<oneshot::Sender<SucceededArtifact<F>>>,
is_once: bool,
) -> Option<CompiledArtifact<F>> {
self.suspended_reason.see(reason);
let reason = std::mem::take(&mut self.suspended_reason);
let start = reflexo::time::now();
let compiling = self.snapshot(is_once, reason);
self.watch_snap = OnceLock::new();
self.watch_snap.get_or_init(|| compiling.clone());
if self.suspended {
self.suspended_reason.see(reason);
for reader in curr_reads.drain(..) {
let _ = reader.send(SucceededArtifact::Suspend(compiling.clone()));
}
return None;
}
if self.compiling {
self.suspended_reason.see(reason);
return None;
}
self.compiling = true;
let h = self.compile_handle.clone();
let curr_reads = std::mem::take(curr_reads);
// todo unwrap main id
let id = compiling.world.main_id().unwrap();
let revision = compiling.world.revision().get();
h.status(revision, CompileReport::Stage(id, "compiling", start));
let compile = move || async move {
let compiled = compiling.compile().await;
for reader in curr_reads {
let _ = reader.send(SucceededArtifact::Compiled(compiled.clone()));
}
let elapsed = start.elapsed().unwrap_or_default();
let rep = match &compiled.doc {
Ok(..) => CompileReport::CompileSuccess(id, compiled.warnings.clone(), elapsed),
Err(err) => CompileReport::CompileError(id, err.clone(), elapsed),
};
let _ = ConsoleDiagReporter::default().export(
&compiled.world,
Arc::new((compiled.env.features.clone(), rep.clone())),
);
// todo: we need to check revision for really concurrent compilation
h.notify_compile(&compiled, rep);
compiled
};
if is_once {
Some(compile().await)
} else {
let intr_tx = self.intr_tx.clone();
tokio::task::spawn(async move {
let err = intr_tx.send(Interrupt::Compiled(compile().await));
log_send_error("compiled", err);
});
None
}
}
fn process_compile(&mut self, artifact: CompiledArtifact<F>, send: impl Fn(CompilerResponse)) {
self.compiling = false;
let mut world = artifact.snap.world;
let compiled_revision = world.revision().get();
if self.committed_revision >= compiled_revision {
return;
}
// Update state.
let doc = artifact.doc.ok();
self.committed_revision = compiled_revision;
self.latest_doc.clone_from(&doc);
if doc.is_some() {
self.latest_success_doc.clone_from(&self.latest_doc);
}
// Notify the new file dependencies.
let mut deps = vec![];
world.iter_dependencies(&mut |dep| {
if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
deps.push(x.into())
}
});
send(CompilerResponse::Notify(NotifyMessage::SyncDependency(
deps,
)));
// Trigger an evict task.
self.cache.evict(world.revision(), world.take_cache());
}
/// Process some interrupt. Return whether it needs compilation.
fn process(&mut self, event: Interrupt<F>, send: impl Fn(CompilerResponse)) -> CompileReasons {
use CompilerResponse::*;
match event {
Interrupt::Compile => {
// Increment the revision anyway.
self.verse.increment_revision(|_| {});
reason_by_entry_change()
}
Interrupt::SnapshotRead(task) => {
log::debug!("CompileServerActor: take snapshot");
if self
.watch_snap
.get()
.is_some_and(|e| e.world.revision() < self.verse.revision)
{
self.watch_snap = OnceLock::new();
}
let _ = task.send(
self.watch_snap
.get_or_init(|| self.snapshot(false, no_reason()))
.clone(),
);
no_reason()
}
Interrupt::CurrentRead(..) => {
unreachable!()
}
Interrupt::ChangeTask(change) => {
self.verse.increment_revision(|verse| {
if let Some(inputs) = change.inputs {
verse.set_inputs(inputs);
}
if let Some(entry) = change.entry.clone() {
let res = verse.mutate_entry(entry);
if let Err(err) = res {
log::error!("CompileServerActor: change entry error: {err:?}");
}
}
});
// After incrementing the revision
if let Some(entry) = change.entry {
self.suspended = entry.is_inactive();
if self.suspended {
log::info!("CompileServerActor: removing diag");
self.compile_handle
.status(self.verse.revision.get(), CompileReport::Suspend);
}
// Reset the watch state and document state.
self.latest_doc = None;
self.latest_success_doc = None;
self.suspended_reason = no_reason();
}
reason_by_entry_change()
}
Interrupt::Compiled(artifact) => {
self.process_compile(artifact, send);
self.process_lagged_compile()
}
Interrupt::Memory(event) => {
log::debug!("CompileServerActor: memory event incoming");
// Emulate memory changes.
let mut files = HashSet::new();
if matches!(event, MemoryEvent::Sync(..)) {
std::mem::swap(&mut files, &mut self.estimated_shadow_files);
}
let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
for path in &e.removes {
self.estimated_shadow_files.remove(path);
files.insert(Arc::clone(path));
}
for (path, _) in &e.inserts {
self.estimated_shadow_files.insert(Arc::clone(path));
files.remove(path);
}
// If there is no invalidation happening, apply memory changes directly.
if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
self.verse
.increment_revision(|verse| Self::apply_memory_changes(verse, event));
return reason_by_mem();
}
// Otherwise, send upstream update event.
// Also, record the logical tick when shadow is dirty.
self.dirty_shadow_logical_tick = self.logical_tick;
send(Notify(NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
invalidates: files.into_iter().collect(),
opaque: Box::new(TaggedMemoryEvent {
logical_tick: self.logical_tick,
event,
}),
})));
no_reason()
}
Interrupt::Fs(mut event) => {
log::debug!("CompileServerActor: fs event incoming {event:?}");
let mut reason = reason_by_fs();
// Apply file system changes.
let dirty_tick = &mut self.dirty_shadow_logical_tick;
self.verse.increment_revision(|verse| {
// Handle delayed upstream update event before applying file system changes
if Self::apply_delayed_memory_changes(verse, dirty_tick, &mut event).is_none() {
log::warn!("CompileServerActor: unknown upstream update event");
// Actual a delayed memory event.
reason = reason_by_mem();
}
verse.vfs().notify_fs_event(event)
});
reason
}
Interrupt::Settle(_) => unreachable!(),
}
}
/// Process reason after each compilation.
fn process_lagged_compile(&mut self) -> CompileReasons {
// The reason which is kept but not used.
std::mem::take(&mut self.suspended_reason)
}
/// Apply delayed memory changes to underlying compiler.
fn apply_delayed_memory_changes(
verse: &mut RevisingUniverse<F>,
dirty_shadow_logical_tick: &mut usize,
event: &mut FilesystemEvent,
) -> Option<()> {
// Handle delayed upstream update event before applying file system changes
if let FilesystemEvent::UpstreamUpdate { upstream_event, .. } = event {
let event = upstream_event.take()?.opaque;
let TaggedMemoryEvent {
logical_tick,
event,
} = *event.downcast().ok()?;
// Recovery from dirty shadow state.
if logical_tick == *dirty_shadow_logical_tick {
*dirty_shadow_logical_tick = 0;
}
Self::apply_memory_changes(verse, event);
}
Some(())
}
/// Apply memory changes to underlying compiler.
fn apply_memory_changes(verse: &mut RevisingUniverse<F>, event: MemoryEvent) {
let mut vfs = verse.vfs();
if matches!(event, MemoryEvent::Sync(..)) {
vfs.reset_shadow();
}
match event {
MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
for path in event.removes {
let _ = vfs.unmap_shadow(&path);
}
for (path, snap) in event.inserts {
let _ = vfs.map_shadow(&path, snap);
}
}
}
}
}
#[inline]
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
res.map_err(|err| log::warn!("CompileServerActor: send to {chan} error: {err}"))
.is_ok()
}

View file

@ -5,13 +5,13 @@ use std::path::PathBuf;
use lsp_server::RequestId;
use lsp_types::*;
use reflexo_typst::error::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use task::TraceParams;
use tinymist_assets::TYPST_PREVIEW_HTML;
use tinymist_query::package::PackageInfo;
use tinymist_query::{ExportKind, LocalContextGuard, PageSelection};
use tinymist_std::error::prelude::*;
use typst::diag::{eco_format, EcoString, StrResult};
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
use world::TaskInputs;
@ -211,9 +211,7 @@ impl LanguageState {
/// Clear all cached resources.
pub fn clear_cache(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
comemo::evict(0);
for dead in self.servers_mut() {
dead.clear_cache();
}
self.project.analysis.clear_cache();
just_ok(JsonValue::Null)
}
@ -285,31 +283,36 @@ impl LanguageState {
}
let previewer = typst_preview::PreviewBuilder::new(cli_args.preview.clone());
let watcher = previewer.compile_watcher();
let primary = self.primary().handle.clone();
if !cli_args.not_as_primary && primary.register_preview(previewer.compile_watcher()) {
let primary = &mut self.project.state.primary;
if !cli_args.not_as_primary && self.preview.watchers.register(&primary.id, watcher) {
let id = primary.id.clone();
// todo: recover pin status reliably
self.pin_entry(Some(entry))
.map_err(|e| internal_error(format!("could not pin file: {e}")))?;
self.preview.start(cli_args, previewer, primary, true)
self.preview.start(cli_args, previewer, id, true)
} else {
self.restart_dedicate(&task_id, Some(entry));
let Some(dedicate) = self.dedicate(&task_id) else {
let id = self
.restart_dedicate(&task_id, Some(entry))
.map_err(internal_error)?;
// Gets the task-dedicated compile server.
let Some(dedicate) = self.project.state.dedicates.iter_mut().find(|d| d.id == id)
else {
return Err(invalid_params(
"just restarted compiler instance for the task is not found",
));
};
let handle = dedicate.handle.clone();
if !handle.register_preview(previewer.compile_watcher()) {
if !self.project.preview.register(&dedicate.id, watcher) {
return Err(invalid_params(
"cannot register preview to the compiler instance",
));
}
self.preview.start(cli_args, previewer, handle, false)
self.preview.start(cli_args, previewer, id, false)
}
}
@ -345,7 +348,7 @@ impl LanguageState {
let from_source = get_arg!(args[0] as String);
let to_path = get_arg!(args[1] as Option<PathBuf>).map(From::from);
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
just_future(async move {
let snap = snap.receive().await.map_err(z_internal_error)?;
@ -390,7 +393,7 @@ impl LanguageState {
let from_source = get_arg!(args[0] as String);
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
just_future(async move {
let snap = snap.receive().await.map_err(z_internal_error)?;
@ -458,7 +461,7 @@ impl LanguageState {
let entry = self.entry_resolver().resolve(Some(path));
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
let user_action = self.user_action;
just_future(async move {
@ -526,13 +529,13 @@ impl LanguageState {
impl LanguageState {
/// Get the all valid fonts
pub fn resource_fonts(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
let snapshot = self.primary().snapshot().map_err(z_internal_error)?;
let snapshot = self.snapshot().map_err(z_internal_error)?;
just_future(Self::get_font_resources(snapshot))
}
/// Get the all valid symbols
pub fn resource_symbols(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
let snapshot = self.primary().snapshot().map_err(z_internal_error)?;
let snapshot = self.snapshot().map_err(z_internal_error)?;
just_future(Self::get_symbol_resources(snapshot))
}
@ -549,7 +552,7 @@ impl LanguageState {
/// Get directory of packages
pub fn resource_package_dirs(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
just_future(async move {
let snap = snap.receive().await.map_err(z_internal_error)?;
let paths = snap.world.registry.paths();
@ -563,7 +566,7 @@ impl LanguageState {
&mut self,
_arguments: Vec<JsonValue>,
) -> AnySchedulableResponse {
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
just_future(async move {
let snap = snap.receive().await.map_err(z_internal_error)?;
let paths = snap.world.registry.local_path();
@ -579,7 +582,7 @@ impl LanguageState {
) -> AnySchedulableResponse {
let ns = get_arg!(arguments[1] as EcoString);
let snap = self.primary().snapshot().map_err(z_internal_error)?;
let snap = self.snapshot().map_err(z_internal_error)?;
just_future(async move {
let snap = snap.receive().await.map_err(z_internal_error)?;
let packages =
@ -597,7 +600,7 @@ impl LanguageState {
&mut self,
mut arguments: Vec<JsonValue>,
) -> AnySchedulableResponse {
let fut = self.primary().query_snapshot().map_err(internal_error)?;
let fut = self.query_snapshot().map_err(internal_error)?;
let info = get_arg!(arguments[1] as PackageInfo);
just_future(async move {
@ -657,7 +660,7 @@ impl LanguageState {
info: PackageInfo,
f: impl FnOnce(&mut LocalContextGuard) -> LspResult<T> + Send + Sync,
) -> LspResult<impl Future<Output = LspResult<T>>> {
let fut = self.primary().query_snapshot().map_err(internal_error)?;
let fut = self.query_snapshot().map_err(internal_error)?;
Ok(async move {
let snap = fut.receive().await.map_err(z_internal_error)?;

View file

@ -21,6 +21,7 @@
mod actor;
mod cmd;
mod init;
pub mod project;
mod resource;
mod server;
mod stats;
@ -33,15 +34,15 @@ mod utils;
pub use init::*;
pub use server::*;
pub use sync_lsp::LspClient;
pub use tinymist_project as project;
pub use tinymist_project::world;
pub use tinymist_query as query;
pub use world::*;
pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs};
use lsp_server::{RequestId, ResponseError};
use serde_json::from_value;
use sync_lsp::*;
use utils::*;
use world::*;
use tinymist_query::CompilerQueryResponse;

View file

@ -28,7 +28,7 @@ use tinymist::{
tool::project::{project_main, task_main},
CompileConfig, Config, LanguageState, RegularInit, SuperInit, UserActionTask,
};
use tinymist::{world::TaskInputs, WorldProvider};
use tinymist::{world::TaskInputs, world::WorldProvider};
use tinymist_core::LONG_VERSION;
use tinymist_query::{package::PackageInfo, EntryResolver};
use typst::foundations::IntoValue;
@ -253,7 +253,7 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> {
let entry = state.entry_resolver().resolve(Some(input.as_path().into()));
let snap = state.primary().snapshot().unwrap();
let snap = state.snapshot().unwrap();
RUNTIMES.tokio_runtime.block_on(async {
let snap = snap.receive().await.unwrap();
@ -274,7 +274,7 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> {
/// The main entry point for language server queries.
pub fn query_main(cmds: QueryCommands) -> anyhow::Result<()> {
use tinymist::package::PackageRegistry;
use tinymist_project::package::PackageRegistry;
with_stdio_transport(MirrorArgs::default(), |conn| {
let client_root = LspClientRoot::new(RUNTIMES.tokio_runtime.handle().clone(), conn.sender);
@ -302,7 +302,7 @@ pub fn query_main(cmds: QueryCommands) -> anyhow::Result<()> {
let state = service.state_mut().unwrap();
let snap = state.primary().snapshot().unwrap();
let snap = state.snapshot().unwrap();
let res = RUNTIMES.tokio_runtime.block_on(async move {
let w = snap.receive().await.map_err(internal_error)?;
match cmds {

View file

@ -0,0 +1,421 @@
//! The project.
//!
//! ```ascii
//! ┌────────────────────────────────┐ ┌────────────┐
//! │ main::compile_actor │◄───────►│notify_actor│
//! └─────┬────────────────────▲─────┘ └────────────┘
//! │ │
//! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐
//! │ compiler::compile_handler ├────────►│ rest actors│
//! └────────────────────────────────┘ └────────────┘
//! ```
//!
//! We use typst by creating a [`ProjectCompiler`] and
//! running compiler with callbacking [`LspProjectHandler`] incrementally. An
//! additional [`LocalCompileHandler`] is also created to control the
//! [`ProjectCompiler`].
//!
//! The [`LspProjectHandler`] will push information to other actors.
#![allow(missing_docs)]
pub use tinymist_project::*;
use std::sync::Arc;
use anyhow::bail;
use log::{error, info, trace};
use parking_lot::Mutex;
use reflexo::{hash::FxHashMap, path::unix_slash};
use reflexo_typst::{typst::prelude::EcoVec, CompileReport};
use sync_lsp::LspClient;
use tinymist_query::{
analysis::{Analysis, AnalysisRevLock, LocalContextGuard},
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, SemanticRequest, StatefulRequest,
VersionedDocument,
};
use tinymist_std::error::prelude::*;
use tokio::sync::{mpsc, oneshot};
use typst::{diag::SourceDiagnostic, World};
use crate::actor::editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum};
use crate::stats::{CompilerQueryStats, QueryStatGuard};
use crate::world::vfs::MemoryEvent;
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
#[derive(Default, Clone)]
pub struct LspPreviewState {
#[cfg(feature = "preview")]
pub(crate) inner: Arc<Mutex<FxHashMap<ProjectInsId, Arc<typst_preview::CompileWatcher>>>>,
}
#[cfg(feature = "preview")]
impl LspPreviewState {
// todo: multiple preview support
#[must_use]
pub fn register(&self, id: &ProjectInsId, handle: &Arc<typst_preview::CompileWatcher>) -> bool {
let mut p = self.inner.lock();
if p.contains_key(id) {
return false;
}
p.insert(id.clone(), handle.clone());
true
}
#[must_use]
pub fn unregister(&self, task_id: &ProjectInsId) -> bool {
let mut p = self.inner.lock();
if p.remove(task_id).is_some() {
return true;
}
false
}
#[must_use]
pub fn get(&self, task_id: &ProjectInsId) -> Option<Arc<typst_preview::CompileWatcher>> {
self.inner.lock().get(task_id).cloned()
}
}
#[derive(Default)]
pub struct ProjectStateExt {
pub is_compiling: bool,
}
/// LSP project compiler.
pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat, ProjectStateExt>;
pub struct Project {
pub diag_group: String,
pub state: LspProjectCompiler,
pub preview: LspPreviewState,
pub analysis: Arc<Analysis>,
pub stats: CompilerQueryStats,
pub export: crate::task::ExportTask,
}
impl Project {
/// Snapshot the compiler thread for tasks
pub fn snapshot(&mut self) -> ZResult<WorldSnapFut> {
let (tx, rx) = oneshot::channel();
let snap = self.state.snapshot();
let _ = tx.send(snap);
Ok(WorldSnapFut { rx })
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&mut self, q: Option<&CompilerQueryRequest>) -> ZResult<QuerySnapFut> {
let fut = self.snapshot()?;
let analysis = self.analysis.clone();
let rev_lock = analysis.lock_revision(q);
Ok(QuerySnapFut {
fut,
analysis,
rev_lock,
})
}
pub fn add_memory_changes(&mut self, event: MemoryEvent) {
self.state.process(Interrupt::Memory(event));
}
pub fn interrupt(&mut self, intr: Interrupt<LspCompilerFeat>) {
if let Interrupt::Compiled(compiled) = &intr {
let proj = self.state.projects().find(|p| p.id == compiled.id);
if let Some(proj) = proj {
proj.ext.is_compiling = false;
}
}
self.state.process(intr);
}
pub fn change_task(&mut self, task: TaskInputs) {
self.state
.process(Interrupt::ChangeTask(self.state.primary.id.clone(), task));
}
pub(crate) fn stop(&mut self) {
// todo: stop all compilations
}
pub(crate) fn restart_dedicate(
&mut self,
group: &str,
entry: EntryState,
) -> ZResult<ProjectInsId> {
self.state.restart_dedicate(group, entry)
}
}
pub struct CompileHandlerImpl {
pub(crate) diag_group: String,
pub(crate) analysis: Arc<Analysis>,
pub(crate) preview: LspPreviewState,
pub(crate) export: crate::task::ExportTask,
pub(crate) editor_tx: EditorSender,
pub(crate) client: Box<dyn ProjectClient>,
pub(crate) notified_revision: parking_lot::Mutex<usize>,
}
pub trait ProjectClient: Send + Sync + 'static {
fn send_event(&self, event: LspInterrupt);
}
impl ProjectClient for LspClient {
fn send_event(&self, event: LspInterrupt) {
self.send_event(event);
}
}
impl ProjectClient for tokio::sync::mpsc::UnboundedSender<LspInterrupt> {
fn send_event(&self, event: LspInterrupt) {
if let Err(err) = self.send(event) {
error!("failed to send interrupt: {err}");
}
}
}
impl CompileHandlerImpl {
fn push_diagnostics(&self, revision: usize, diagnostics: Option<DiagnosticsMap>) {
let dv = DocVersion {
group: self.diag_group.clone(),
revision,
};
let res = self.editor_tx.send(EditorRequest::Diag(dv, diagnostics));
if let Err(err) = res {
error!("failed to send diagnostics: {err:#}");
}
}
fn notify_diagnostics(
&self,
world: &LspWorld,
errors: EcoVec<SourceDiagnostic>,
warnings: EcoVec<SourceDiagnostic>,
) {
let revision = world.revision().get();
trace!("notify diagnostics({revision}): {errors:#?} {warnings:#?}");
let diagnostics = tinymist_query::convert_diagnostics(
world,
errors.iter().chain(warnings.iter()),
self.analysis.position_encoding,
);
let entry = world.entry_state();
// todo: better way to remove diagnostics
// todo: check all errors in this file
let detached = entry.is_inactive();
let valid = !detached;
self.push_diagnostics(revision, valid.then_some(diagnostics));
}
}
impl CompileHandler<LspCompilerFeat, ProjectStateExt> for CompileHandlerImpl {
fn on_any_compile_reason(&self, c: &mut LspProjectCompiler) {
let instances_mut = std::iter::once(&mut c.primary).chain(c.dedicates.iter_mut());
for s in instances_mut {
if s.ext.is_compiling {
continue;
}
let Some(compile_fn) = s.may_compile(&c.handler) else {
continue;
};
s.ext.is_compiling = true;
rayon::spawn(move || {
compile_fn();
});
}
}
fn status(&self, revision: usize, id: &ProjectInsId, rep: CompileReport) {
// todo: seems to duplicate with CompileStatus
let status = match rep {
CompileReport::Suspend => {
self.push_diagnostics(revision, None);
TinymistCompileStatusEnum::CompileSuccess
}
CompileReport::Stage(_, _, _) => TinymistCompileStatusEnum::Compiling,
CompileReport::CompileSuccess(_, _, _) => TinymistCompileStatusEnum::CompileSuccess,
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
TinymistCompileStatusEnum::CompileError
}
};
let this = &self;
this.editor_tx
.send(EditorRequest::Status(CompileStatus {
group: this.diag_group.clone(),
path: rep
.compiling_id()
.map(|s| unix_slash(s.vpath().as_rooted_path()))
.unwrap_or_default(),
status,
}))
.unwrap();
#[cfg(feature = "preview")]
if let Some(inner) = this.preview.get(id) {
use typst_preview::CompileStatus;
let status = match rep {
CompileReport::Suspend => CompileStatus::CompileSuccess,
CompileReport::Stage(_, _, _) => CompileStatus::Compiling,
CompileReport::CompileSuccess(_, _, _) => CompileStatus::CompileSuccess,
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
CompileStatus::CompileError
}
};
inner.status(status);
}
}
fn notify_compile(&self, snap: &CompiledArtifact<LspCompilerFeat>, rep: CompileReport) {
// todo: we need to manage the revision for fn status() as well
{
let mut n_rev = self.notified_revision.lock();
if *n_rev >= snap.world.revision().get() {
log::info!(
"TypstActor: already notified for revision {} <= {n_rev}",
snap.world.revision(),
);
return;
}
*n_rev = snap.world.revision().get();
}
self.notify_diagnostics(
&snap.world,
snap.doc.clone().err().unwrap_or_default(),
snap.warnings.clone(),
);
self.client.send_event(LspInterrupt::Compiled(snap.clone()));
self.export.signal(snap, snap.signal);
self.editor_tx
.send(EditorRequest::Status(CompileStatus {
group: self.diag_group.clone(),
path: rep
.compiling_id()
.map(|s| unix_slash(s.vpath().as_rooted_path()))
.unwrap_or_default(),
status: if snap.doc.is_ok() {
TinymistCompileStatusEnum::CompileSuccess
} else {
TinymistCompileStatusEnum::CompileError
},
}))
.unwrap();
#[cfg(feature = "preview")]
if let Some(inner) = self.preview.get(&snap.id) {
let snap = snap.clone();
inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { snap }));
}
}
}
pub struct QuerySnapWithStat {
pub fut: QuerySnapFut,
pub(crate) stat: QueryStatGuard,
}
pub struct WorldSnapFut {
rx: oneshot::Receiver<CompileSnapshot<LspCompilerFeat>>,
}
impl WorldSnapFut {
/// wait for the snapshot to be ready
pub async fn receive(self) -> ZResult<CompileSnapshot<LspCompilerFeat>> {
self.rx
.await
.map_err(map_string_err("failed to get snapshot"))
}
}
pub struct QuerySnapFut {
fut: WorldSnapFut,
analysis: Arc<Analysis>,
rev_lock: AnalysisRevLock,
}
impl QuerySnapFut {
/// wait for the snapshot to be ready
pub async fn receive(self) -> ZResult<QuerySnap> {
let snap = self.fut.receive().await?;
Ok(QuerySnap {
snap,
analysis: self.analysis,
rev_lock: self.rev_lock,
})
}
}
pub struct QuerySnap {
pub snap: CompileSnapshot<LspCompilerFeat>,
analysis: Arc<Analysis>,
rev_lock: AnalysisRevLock,
}
impl std::ops::Deref for QuerySnap {
type Target = CompileSnapshot<LspCompilerFeat>;
fn deref(&self) -> &Self::Target {
&self.snap
}
}
impl QuerySnap {
pub fn task(mut self, inputs: TaskInputs) -> Self {
self.snap = self.snap.task(inputs);
self
}
pub fn run_stateful<T: StatefulRequest>(
self,
query: T,
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
) -> anyhow::Result<CompilerQueryResponse> {
let doc = self.snap.success_doc.as_ref().map(|doc| VersionedDocument {
version: self.world.revision().get(),
document: doc.clone(),
});
self.run_analysis(|ctx| query.request(ctx, doc))
.map(wrapper)
}
pub fn run_semantic<T: SemanticRequest>(
self,
query: T,
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
) -> anyhow::Result<CompilerQueryResponse> {
self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
}
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");
bail!("main file is not set");
};
world.source(main).map_err(|err| {
info!("TypstActor: failed to prepare main file: {err:?}");
anyhow::anyhow!("failed to get source: {err}")
})?;
let mut analysis = self.analysis.snapshot_(world, self.rev_lock);
Ok(f(&mut analysis))
}
}

View file

@ -5,7 +5,7 @@ use tinymist_std::debug_loc::DataSource;
use typst::text::{FontStretch, FontStyle, FontWeight};
use super::prelude::*;
use crate::actor::typ_client::WorldSnapFut;
use crate::project::WorldSnapFut;
use crate::world::font::FontResolver;
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -6,13 +6,13 @@ mod prelude {
pub use std::collections::HashMap;
pub use once_cell::sync::Lazy;
pub use reflexo_typst::error::prelude::*;
pub use reflexo_typst::Compiler;
pub use reflexo_vec2svg::ir::{GlyphItem, GlyphRef};
pub use reflexo_vec2svg::{DefaultExportFeature, SvgTask, SvgText};
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value as JsonValue;
pub use sync_lsp::*;
pub use tinymist_std::error::prelude::*;
pub use typst::foundations::{Scope, Value};
pub use typst::symbols::Symbol;

View file

@ -3,11 +3,10 @@ use std::{collections::BTreeMap, path::Path, sync::Arc};
// use reflexo_typst::font::GlyphId;
use reflexo_typst::{vector::font::GlyphId, TypstDocument, TypstFont};
use sync_lsp::LspResult;
use typst::syntax::VirtualPath;
use typst::World;
use typst::{syntax::VirtualPath, World};
use crate::world::{base::ShadowApi, EntryState, TaskInputs};
use crate::{actor::typ_client::WorldSnapFut, z_internal_error};
use crate::{project::WorldSnapFut, z_internal_error};
use super::prelude::*;

View file

@ -1,10 +1,10 @@
//! tinymist's language server
use std::collections::HashMap;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use actor::editor::EditorActor;
use anyhow::anyhow;
use anyhow::Context;
@ -13,24 +13,41 @@ use lsp_server::RequestId;
use lsp_types::request::{GotoDeclarationParams, WorkspaceConfiguration};
use lsp_types::*;
use once_cell::sync::OnceCell;
use reflexo_typst::{error::prelude::*, Bytes, Error, ImmutPath};
use prelude::*;
use project::world::EntryState;
use project::{watch_deps, LspPreviewState};
use project::{CompileHandlerImpl, Project, QuerySnapFut, QuerySnapWithStat, WorldSnapFut};
use reflexo_typst::Bytes;
use request::{RegisterCapability, UnregisterCapability};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue};
use sync_lsp::*;
use task::{CacheTask, ExportUserConfig, FormatTask, FormatterConfig, UserActionTask};
use task::{
ExportConfig, ExportTask, ExportUserConfig, FormatTask, FormatterConfig, UserActionTask,
};
use tinymist_project::ProjectInsId;
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
use tinymist_query::{
to_typst_range, CompilerQueryRequest, CompilerQueryResponse, FoldRequestFeature,
PositionEncoding, SyntaxRequest,
OnExportRequest, PositionEncoding, ServerInfoResponse, SyntaxRequest,
};
use tinymist_query::{EntryResolver, PageSelection};
use tinymist_query::{ExportKind, LocalContext, VersionedDocument};
use tinymist_render::PeriscopeRenderer;
use tinymist_std::Error;
use tinymist_std::ImmutPath;
use tokio::sync::mpsc;
use typst::layout::Position as TypstPosition;
use typst::{diag::FileResult, syntax::Source};
use crate::project::LspInterrupt;
use crate::project::{CompileServerOpts, ProjectCompiler};
use crate::stats::CompilerQueryStats;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use crate::world::{ImmutDict, LspUniverseBuilder, TaskInputs};
use super::{init::*, *};
use crate::actor::editor::EditorRequest;
use crate::actor::typ_client::CompileClientActor;
use crate::world::TaskInputs;
pub(crate) use futures::Future;
@ -50,6 +67,8 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) {
pub struct LanguageState {
/// The lsp client
pub client: TypedLspClient<Self>,
/// The project state.
pub project: Project,
// State to synchronize with the client.
/// Whether the server has registered semantic tokens capabilities.
@ -77,18 +96,12 @@ pub struct LanguageState {
pub preview: tool::preview::PreviewState,
/// The diagnostics sender to send diagnostics to `crate::actor::cluster`.
pub editor_tx: mpsc::UnboundedSender<EditorRequest>,
/// The primary compiler actor.
pub primary: Option<CompileClientActor>,
/// The compiler actors for tasks
pub dedicates: Vec<CompileClientActor>,
/// The formatter tasks running in backend, which will be scheduled by async
/// runtime.
pub formatter: FormatTask,
/// The user action tasks running in backend, which will be scheduled by
/// async runtime.
pub user_action: UserActionTask,
/// The cache task running in backend
pub cache: CacheTask,
}
/// Getters and the main loop.
@ -101,14 +114,25 @@ impl LanguageState {
) -> Self {
let formatter = FormatTask::new(config.formatter());
let default_path = config.compile.entry_resolver.resolve_default();
let watchers = LspPreviewState::default();
let handle = Self::server(
&config,
editor_tx.clone(),
client.clone(),
"primary".to_string(),
config.compile.entry_resolver.resolve(default_path),
config.compile.determine_inputs(),
watchers.clone(),
);
Self {
client: client.clone(),
project: handle,
editor_tx,
primary: None,
dedicates: Vec::new(),
memory_changes: HashMap::new(),
#[cfg(feature = "preview")]
preview: tool::preview::PreviewState::new(client.cast(|s| &mut s.preview)),
preview: tool::preview::PreviewState::new(watchers, client.cast(|s| &mut s.preview)),
ever_focusing_by_activities: false,
ever_manual_focusing: false,
sema_tokens_registered: false,
@ -119,7 +143,6 @@ impl LanguageState {
focusing: None,
formatter,
user_action: Default::default(),
cache: CacheTask::default(),
}
}
@ -139,7 +162,10 @@ impl LanguageState {
service.config.compile.notify_status,
);
service.restart_primary();
let err = service.restart_primary();
if let Err(err) = err {
error!("could not restart primary: {err}");
}
// Run the cluster in the background after we referencing it
client.handle.spawn(editor_actor.run());
@ -158,30 +184,8 @@ impl LanguageState {
&self.config.compile
}
/// Get the entry resolver.
pub fn entry_resolver(&self) -> &EntryResolver {
&self.compile_config().entry_resolver
}
/// Get the primary compile server for those commands without task context.
pub fn primary(&self) -> &CompileClientActor {
self.primary.as_ref().expect("primary")
}
/// Get the task-dedicated compile server.
pub fn dedicate(&self, group: &str) -> Option<&CompileClientActor> {
self.dedicates
.iter()
.find(|dedicate| dedicate.handle.diag_group == group)
}
/// Get all compile servers in current state.
pub fn servers_mut(&mut self) -> impl Iterator<Item = &mut CompileClientActor> {
self.primary.iter_mut().chain(self.dedicates.iter_mut())
}
/// Install handlers to the language server.
pub fn install<T: Initializer<S = Self> + AddCommands>(
pub fn install<T: Initializer<S = Self> + AddCommands + 'static>(
provider: LspBuilder<T>,
) -> LspBuilder<T> {
type State = LanguageState;
@ -197,6 +201,11 @@ impl LanguageState {
// todo: .on_sync_mut::<notifs::Cancel>(handlers::handle_cancel)?
let mut provider = provider
.with_request::<Shutdown>(State::shutdown)
// customized event
.with_event(
&LspInterrupt::Compile(ProjectInsId::default()),
State::compile_interrupt::<T>,
)
// lantency sensitive
.with_request_::<Completion>(State::completion)
.with_request_::<SemanticTokensFullRequest>(State::semantic_tokens_full)
@ -273,17 +282,20 @@ impl LanguageState {
provider
}
/// Get all sources in current state.
pub fn vfs_snapshot(&self) -> FileChangeSet {
FileChangeSet::new_inserts(
self.memory_changes
.iter()
.map(|(path, meta)| {
let content = meta.content.clone().text().as_bytes().into();
(path.clone(), FileResult::Ok(content).into())
})
.collect(),
)
fn compile_interrupt<T: Initializer<S = Self>>(
mut state: ServiceState<T, T::S>,
params: LspInterrupt,
) -> anyhow::Result<()> {
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());
Ok(())
}
}
@ -526,10 +538,6 @@ impl LanguageState {
}
}
for e in self.primary.iter_mut().chain(self.dedicates.iter_mut()) {
e.sync_config(self.config.compile.clone());
}
if config.compile.output_path != self.config.compile.output_path
|| config.compile.export_pdf != self.config.compile.export_pdf
{
@ -538,16 +546,15 @@ impl LanguageState {
mode: self.config.compile.export_pdf,
};
self.primary
.as_mut()
.unwrap()
.change_export_config(config.clone());
self.change_export_config(config.clone());
}
if config.compile.primary_opts() != self.config.compile.primary_opts() {
self.config.compile.fonts = OnceCell::new(); // todo: don't reload fonts if not changed
self.restart_primary();
// todo: restart dedicates
let err = self.restart_primary();
if let Err(err) = err {
error!("could not restart primary: {err}");
}
}
if config.semantic_tokens != self.config.semantic_tokens {
@ -836,11 +843,24 @@ impl LanguageState {
impl LanguageState {
/// Focus main file to some path.
pub fn do_change_entry(&mut self, new_entry: Option<ImmutPath>) -> Result<bool, Error> {
self.primary
.as_mut()
.unwrap()
.change_entry(new_entry.clone())
pub fn change_entry(&mut self, path: Option<ImmutPath>) -> Result<bool, Error> {
if path
.as_deref()
.is_some_and(|p| !p.is_absolute() && !p.starts_with("/untitled"))
{
return Err(error_once!("entry file must be absolute", path: path.unwrap().display()));
}
let next_entry = self.entry_resolver().resolve(path);
info!("the entry file of TypstActor(primary) is changing to {next_entry:?}");
self.change_task(TaskInputs {
entry: Some(next_entry.clone()),
..Default::default()
});
Ok(true)
}
/// Pin the entry to the given path
@ -849,7 +869,7 @@ impl LanguageState {
let entry = new_entry
.or_else(|| self.entry_resolver().resolve_default())
.or_else(|| self.focusing.clone());
self.do_change_entry(entry).map(|_| ())
self.change_entry(entry).map(|_| ())
}
/// Updates the primary (focusing) entry
@ -859,7 +879,7 @@ impl LanguageState {
return Ok(false);
}
self.do_change_entry(new_entry.clone())
self.change_entry(new_entry.clone())
}
/// This is used for tracking activating document status if a client is not
@ -905,13 +925,295 @@ impl LanguageState {
Ok(false) => {}
}
}
/// Snapshot the compiler thread for tasks
pub fn snapshot(&mut self) -> ZResult<WorldSnapFut> {
self.project.snapshot()
}
/// Get the entry resolver.
pub fn entry_resolver(&self) -> &EntryResolver {
&self.compile_config().entry_resolver
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&mut self) -> ZResult<QuerySnapFut> {
self.project.query_snapshot(None)
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot_with_stat(
&mut self,
q: &CompilerQueryRequest,
) -> ZResult<QuerySnapWithStat> {
let name: &'static str = q.into();
let path = q.associated_path();
let stat = self.project.stats.query_stat(path, name);
let fut = self.project.query_snapshot(Some(q))?;
Ok(QuerySnapWithStat { fut, stat })
}
fn add_memory_changes(&mut self, event: MemoryEvent) {
self.project.add_memory_changes(event);
}
fn change_task(&mut self, task_inputs: TaskInputs) {
self.project.change_task(task_inputs);
}
pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
self.project.export.change_config(config);
}
// pub async fn settle(&mut self) {
// let _ = self.change_entry(None);
// info!("TypstActor({}): settle requested", self.handle.diag_group);
// match self.handle.settle().await {
// Ok(()) => info!("TypstActor({}): settled", self.handle.diag_group),
// Err(err) => error!(
// "TypstActor({}): failed to settle: {err:#}",
// self.handle.diag_group
// ),
// }
// }
/// Get the current server info.
pub fn collect_server_info(&mut self) -> QueryFuture {
let dg = self.project.diag_group.clone();
let api_stats = self.project.stats.report();
let query_stats = self.project.analysis.report_query_stats();
let alloc_stats = self.project.analysis.report_alloc_stats();
let snap = self.snapshot()?;
just_future(async move {
let snap = snap.receive().await?;
let w = &snap.world;
let info = ServerInfoResponse {
root: w.entry_state().root().map(|e| e.as_ref().to_owned()),
font_paths: w.font_resolver.font_paths().to_owned(),
inputs: w.inputs().as_ref().deref().clone(),
stats: HashMap::from_iter([
("api".to_owned(), api_stats),
("query".to_owned(), query_stats),
("alloc".to_owned(), alloc_stats),
]),
};
let info = Some(HashMap::from_iter([(dg, info)]));
Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info))
})
}
/// Export the current document.
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, kind, open } = req;
let snap = self.snapshot()?;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let export = self.project.export.factory.oneshot(snap, Some(entry), kind);
just_future(async move {
let res = export.await?;
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
// Also see https://github.com/Byron/open-rs/issues/105
#[cfg(not(target_os = "windows"))]
let do_open = ::open::that_detached;
#[cfg(target_os = "windows")]
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
::open::with_detached(path, "explorer")
}
if let Some(Some(path)) = open.then_some(res.as_ref()) {
log::info!("open with system default apps: {path:?}");
if let Err(e) = do_open(path) {
log::error!("failed to open with system default apps: {e}");
};
}
log::info!("CompileActor: on export end: {path:?} as {res:?}");
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
})
}
}
impl LanguageState {
/// Restart the primary server.
pub fn restart_primary(&mut self) -> ZResult<ProjectInsId> {
let entry = self.entry_resolver().resolve_default();
let config = &self.config;
// todo: hot replacement
#[cfg(feature = "preview")]
self.preview.stop_all();
let new_project = Self::server(
config,
self.editor_tx.clone(),
self.client.clone(),
"primary".to_string(),
config.compile.entry_resolver.resolve(entry),
config.compile.determine_inputs(),
self.preview.watchers.clone(),
);
let mut old_project = std::mem::replace(&mut self.project, new_project);
rayon::spawn(move || {
old_project.stop();
});
Ok(self.project.state.primary.id.clone())
}
/// Restart the server with the given group.
pub fn restart_dedicate(
&mut self,
dedicate: &str,
entry: Option<ImmutPath>,
) -> ZResult<ProjectInsId> {
let entry = self.config.compile.entry_resolver.resolve(entry);
self.project.restart_dedicate(dedicate, entry)
}
/// Create a new server for the given group.
pub fn server(
config: &Config,
editor_tx: tokio::sync::mpsc::UnboundedSender<EditorRequest>,
client: TypedLspClient<LanguageState>,
diag_group: String,
entry: EntryState,
inputs: ImmutDict,
preview: project::LspPreviewState,
) -> Project {
let compile_config = &config.compile;
let const_config = &config.const_config;
// use codespan_reporting::term::Config;
// Run Export actors before preparing cluster to avoid loss of events
let export_config = ExportConfig {
group: diag_group.clone(),
editor_tx: Some(editor_tx.clone()),
config: ExportUserConfig {
output: compile_config.output_path.clone(),
mode: compile_config.export_pdf,
},
kind: ExportKind::Pdf {
creation_timestamp: config.compile.determine_creation_timestamp(),
},
count_words: config.compile.notify_status,
};
let export = ExportTask::new(client.handle.clone(), export_config);
log::info!(
"TypstActor: creating server for {diag_group}, entry: {entry:?}, inputs: {inputs:?}"
);
// Create the compile handler for client consuming results.
let periscope_args = compile_config.periscope_args.clone();
let handle = Arc::new(CompileHandlerImpl {
#[cfg(feature = "preview")]
preview,
diag_group: diag_group.clone(),
export: export.clone(),
editor_tx: editor_tx.clone(),
client: Box::new(client.clone().to_untyped()),
analysis: Arc::new(Analysis {
position_encoding: const_config.position_encoding,
allow_overlapping_token: const_config.tokens_overlapping_token_support,
allow_multiline_token: const_config.tokens_multiline_token_support,
remove_html: !config.support_html_in_markdown,
completion_feat: config.completion.clone(),
color_theme: match compile_config.color_theme.as_deref() {
Some("dark") => tinymist_query::ColorTheme::Dark,
_ => tinymist_query::ColorTheme::Light,
},
periscope: periscope_args.map(|args| {
let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args));
Arc::new(r) as Arc<dyn PeriscopeProvider + Send + Sync>
}),
tokens_caches: Arc::default(),
workers: Default::default(),
caches: Default::default(),
analysis_rev_cache: Arc::default(),
stats: Arc::default(),
}),
notified_revision: parking_lot::Mutex::new(0),
});
let font_resolver = compile_config.determine_fonts();
let entry_ = entry.clone();
let compile_handle = handle.clone();
let cert_path = compile_config.determine_certification_path();
let package = compile_config.determine_package_opts();
// todo: never fail?
let default_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap());
let package_registry =
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
let verse =
LspUniverseBuilder::build(entry_.clone(), inputs, default_fonts, package_registry)
.expect("incorrect options");
// todo: unify filesystem watcher
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
let fs_client = client.clone().to_untyped();
let async_handle = client.handle.clone();
async_handle.spawn(watch_deps(dep_rx, move |event| {
fs_client.send_event(LspInterrupt::Fs(event));
}));
// Create the actor
let server = ProjectCompiler::new(
verse,
dep_tx,
CompileServerOpts {
handler: compile_handle,
enable_watch: true,
..Default::default()
},
);
let font_client = client.clone();
client.handle.spawn_blocking(move || {
// Create the world
let font_resolver = font_resolver.wait().clone();
font_client.send_event(LspInterrupt::Font(font_resolver));
});
// todo: restart loses the memory changes
// We do send memory changes instead of initializing compiler with them.
// This is because there are state recorded inside of the compiler actor, and we
// must update them.
// client.add_memory_changes(MemoryEvent::Update(snapshot));
Project {
diag_group,
state: server,
preview: Default::default(),
analysis: handle.analysis.clone(),
stats: CompilerQueryStats::default(),
export: handle.export.clone(),
}
}
}
struct TypstPeriscopeProvider(PeriscopeRenderer);
impl PeriscopeProvider for TypstPeriscopeProvider {
/// Resolve periscope image at the given position.
fn periscope_at(
&self,
ctx: &mut LocalContext,
doc: VersionedDocument,
pos: TypstPosition,
) -> Option<String> {
self.0.render_marked(ctx, doc, pos)
}
}
impl LanguageState {
fn update_source(&mut self, files: FileChangeSet) -> Result<(), Error> {
for srv in self.servers_mut() {
srv.add_memory_changes(MemoryEvent::Update(files.clone()));
}
self.add_memory_changes(MemoryEvent::Update(files.clone()));
Ok(())
}
@ -920,6 +1222,7 @@ impl LanguageState {
pub fn create_source(&mut self, path: PathBuf, content: String) -> Result<(), Error> {
let path: ImmutPath = path.into();
log::info!("create source: {path:?}");
self.memory_changes.insert(
path.clone(),
MemoryFileMeta {
@ -928,7 +1231,6 @@ impl LanguageState {
);
let content: Bytes = content.as_bytes().into();
log::info!("create source: {:?}", path);
// todo: is it safe to believe that the path is normalized?
let files = FileChangeSet::new_inserts(vec![(path, FileResult::Ok(content).into())]);
@ -1014,7 +1316,6 @@ impl LanguageState {
pub fn query(&mut self, query: CompilerQueryRequest) -> QueryFuture {
use CompilerQueryRequest::*;
let primary = || self.primary();
let is_pinning = self.pinning;
just_ok(match query {
FoldingRange(req) => query_source!(self, FoldingRange, req)?,
@ -1022,27 +1323,24 @@ impl LanguageState {
DocumentSymbol(req) => query_source!(self, DocumentSymbol, req)?,
OnEnter(req) => query_source!(self, OnEnter, req)?,
ColorPresentation(req) => CompilerQueryResponse::ColorPresentation(req.request()),
OnExport(req) => return primary().on_export(req),
ServerInfo(_) => return primary().collect_server_info(),
_ => return Self::query_on(primary(), is_pinning, query),
OnExport(req) => return self.on_export(req),
ServerInfo(_) => return self.collect_server_info(),
// todo: query on dedicate projects
_ => return self.query_on(is_pinning, query),
})
}
fn query_on(
client: &CompileClientActor,
is_pinning: bool,
query: CompilerQueryRequest,
) -> QueryFuture {
fn query_on(&mut self, is_pinning: bool, query: CompilerQueryRequest) -> QueryFuture {
use CompilerQueryRequest::*;
type R = CompilerQueryResponse;
assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique);
let fut_stat = client.query_snapshot_with_stat(&query)?;
let fut_stat = self.query_snapshot_with_stat(&query)?;
let entry = query
.associated_path()
.map(|path| client.entry_resolver().resolve(Some(path.into())))
.map(|path| self.entry_resolver().resolve(Some(path.into())))
.or_else(|| {
let root = client.entry_resolver().root(None)?;
let root = self.entry_resolver().root(None)?;
Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY))
});

View file

@ -69,7 +69,7 @@ pub struct CompilerQueryStats {
impl CompilerQueryStats {
/// Record a query.
pub fn query_stat(&self, path: Option<&Path>, name: &'static str) -> QueryStatGuard {
pub(crate) fn query_stat(&self, path: Option<&Path>, name: &'static str) -> QueryStatGuard {
let stats = &self.query_stats;
// let refs = stats.entry(path.clone()).or_default();
let refs = stats

View file

@ -1,64 +0,0 @@
//! The actor that handles cache evicting.
use std::{
num::NonZeroUsize,
sync::{atomic::AtomicUsize, Arc},
};
use crate::world::vfs::SourceCache;
use super::{FutureFolder, SyncTaskFactory};
#[derive(Debug, Clone)]
pub struct CacheUserConfig {
pub max_age: usize,
pub vfs_age: usize,
}
impl Default for CacheUserConfig {
fn default() -> Self {
Self {
max_age: 30,
vfs_age: 15,
}
}
}
#[derive(Clone, Default)]
pub struct CacheTask {
factory: SyncTaskFactory<CacheUserConfig>,
cache_evict_folder: FutureFolder,
revision: Arc<AtomicUsize>,
}
impl CacheTask {
pub fn new(c: CacheUserConfig) -> Self {
Self {
factory: SyncTaskFactory::new(c),
cache_evict_folder: FutureFolder::default(),
revision: Arc::new(AtomicUsize::default()),
}
}
pub fn evict(&self, rev: NonZeroUsize, source_cache: SourceCache) {
let revision = self
.revision
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let task = self.factory.task();
self.cache_evict_folder.spawn(revision, || {
Box::pin(async move {
let _ = FutureFolder::compute(move |_| {
// Evict compilation cache.
let evict_start = std::time::Instant::now();
comemo::evict(task.max_age);
source_cache.evict(rev, task.vfs_age);
let elapsed = evict_start.elapsed();
log::info!("CacheEvictTask: evict cache in {elapsed:?}");
})
.await;
Some(())
})
});
}
}

View file

@ -3,10 +3,10 @@
use std::str::FromStr;
use std::{path::PathBuf, sync::Arc};
use crate::world::TaskInputs;
use crate::world::{EntryReader, EntryState};
use crate::project::{CompiledArtifact, ExportSignal};
use anyhow::{bail, Context};
use reflexo_typst::TypstDatetime;
use tinymist_project::{EntryReader, EntryState, TaskInputs};
use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::mpsc;
use typlite::Typlite;
@ -20,13 +20,7 @@ use typst_pdf::PdfOptions;
use crate::tool::text::FullTextDigest;
use crate::{
actor::{
editor::EditorRequest,
typ_client::WorldSnapFut,
typ_server::{CompiledArtifact, ExportSignal},
},
tool::word_count,
world::LspCompilerFeat,
actor::editor::EditorRequest, project::WorldSnapFut, tool::word_count, world::LspCompilerFeat,
ExportMode, PathPattern,
};
@ -41,18 +35,21 @@ pub struct ExportUserConfig {
pub mode: ExportMode,
}
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct ExportTask {
pub handle: tokio::runtime::Handle,
pub factory: SyncTaskFactory<ExportConfig>,
export_folder: FutureFolder,
count_word_folder: FutureFolder,
}
impl ExportTask {
pub fn new(data: ExportConfig) -> Self {
pub fn new(handle: tokio::runtime::Handle, data: ExportConfig) -> Self {
Self {
handle,
factory: SyncTaskFactory::new(data),
..ExportTask::default()
export_folder: FutureFolder::default(),
count_word_folder: FutureFolder::default(),
}
}
@ -81,7 +78,7 @@ impl SyncTaskFactory<ExportConfig> {
..Default::default()
});
let artifact = snap.compile().await;
let artifact = snap.compile();
export.do_export(&kind, artifact).await
}
}
@ -128,29 +125,33 @@ impl ExportConfig {
return None;
}
t.export_folder.spawn(artifact.world.revision().get(), || {
let fut = t.export_folder.spawn(artifact.world.revision().get(), || {
let this = self.clone();
let artifact = artifact.clone();
Box::pin(async move {
log_err(this.do_export(&this.kind, artifact).await);
Some(())
})
});
})?;
t.handle.spawn(fut);
Some(())
}
fn signal_count_word(&self, artifact: &CompiledArtifact<LspCompilerFeat>, t: &ExportTask) {
fn signal_count_word(
&self,
artifact: &CompiledArtifact<LspCompilerFeat>,
t: &ExportTask,
) -> Option<()> {
if !self.count_words {
return;
return None;
}
let Some(editor_tx) = self.editor_tx.clone() else {
return;
};
let editor_tx = self.editor_tx.clone()?;
let revision = artifact.world.revision().get();
t.count_word_folder.spawn(revision, || {
let fut = t.count_word_folder.spawn(revision, || {
let artifact = artifact.clone();
let group = self.group.clone();
Box::pin(async move {
@ -165,7 +166,11 @@ impl ExportConfig {
Some(())
})
});
})?;
t.handle.spawn(fut);
Some(())
}
async fn do_export(

View file

@ -8,8 +8,6 @@ mod format;
pub use format::*;
mod user_action;
pub use user_action::*;
mod cache;
pub use cache::*;
use std::{ops::DerefMut, pin::Pin, sync::Arc};
@ -64,7 +62,12 @@ impl FutureFolder {
.map_err(|e| anyhow::anyhow!("compute error: {e:?}"))
}
fn spawn(&self, revision: usize, fut: impl FnOnce() -> FoldFuture) {
#[must_use]
fn spawn(
&self,
revision: usize,
fut: impl FnOnce() -> FoldFuture,
) -> Option<impl Future<Output = ()> + Send + 'static> {
let mut state = self.state.lock();
let state = state.deref_mut();
@ -75,7 +78,7 @@ impl FutureFolder {
*prev_revision = revision;
}
return;
return None;
}
next_update => {
*next_update = Some((revision, fut()));
@ -83,13 +86,13 @@ impl FutureFolder {
}
if state.running {
return;
return None;
}
state.running = true;
let state = self.state.clone();
tokio::spawn(async move {
Some(async move {
loop {
let fut = {
let mut state = state.lock();
@ -101,6 +104,6 @@ impl FutureFolder {
};
fut.await;
}
});
})
}
}

View file

@ -178,7 +178,7 @@ async fn trace_main(
let timings = writer.into_inner().unwrap();
let handle = &state.primary().handle;
let handle = &state.project;
let diagnostics =
tinymist_query::convert_diagnostics(w, diags.iter(), handle.analysis.position_encoding);

View file

@ -1,9 +1,11 @@
//! Document preview tool for Typst
#![allow(missing_docs)]
use std::num::NonZeroUsize;
use std::ops::DerefMut;
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use futures::{SinkExt, StreamExt, TryStreamExt};
use hyper::service::service_fn;
use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStream};
@ -16,6 +18,7 @@ use serde::Serialize;
use serde_json::Value as JsonValue;
use sync_lsp::just_ok;
use tinymist_assets::TYPST_PREVIEW_HTML;
use tinymist_project::ProjectInsId;
use tokio::sync::{mpsc, oneshot};
use typst::layout::{Frame, FrameItem, Point, Position};
use typst::syntax::{LinkedNode, Source, Span, SyntaxKind};
@ -28,11 +31,15 @@ use typst_preview::{
};
use typst_shim::syntax::LinkedNodeExt;
use crate::project::{
CompileHandlerImpl, CompileServerOpts, CompiledArtifact, LspInterrupt, ProjectClient,
ProjectCompiler,
};
use crate::world::LspCompilerFeat;
use crate::*;
use actor::preview::{PreviewActor, PreviewRequest, PreviewTab};
use actor::typ_client::CompileHandler;
use actor::typ_server::{CompileServerActor, CompileServerOpts, CompiledArtifact};
use project::world::vfs::{notify::MemoryEvent, FileChangeSet};
use project::{watch_deps, LspPreviewState};
/// The preview's view of the compiled artifact.
pub struct PreviewCompileView {
@ -63,6 +70,7 @@ impl typst_preview::CompileView for PreviewCompileView {
let Location::Src(loc) = loc;
let source_id = world.id_for_path(Path::new(&loc.filepath))?;
let source = world.source(source_id).ok()?;
let cursor = source.line_column_to_byte(loc.pos.line, loc.pos.column)?;
@ -88,10 +96,10 @@ impl typst_preview::CompileView for PreviewCompileView {
let Some(doc) = doc.as_deref() else {
return vec![];
};
let Some(source_id) = world.id_for_path(Path::new(&src_loc.filepath)) else {
return vec![];
};
let Some(source) = world.source(source_id).ok() else {
return vec![];
};
@ -123,42 +131,6 @@ impl typst_preview::CompileView for PreviewCompileView {
}
}
impl EditorServer for CompileHandler {
async fn update_memory_files(
&self,
files: MemoryFiles,
reset_shadow: bool,
) -> Result<(), Error> {
// todo: is it safe to believe that the path is normalized?
let files = FileChangeSet::new_inserts(
files
.files
.into_iter()
.map(|(path, content)| {
let content = content.as_bytes().into();
// todo: cloning PathBuf -> Arc<Path>
(path.into(), Ok(content).into())
})
.collect(),
);
self.add_memory_changes(if reset_shadow {
MemoryEvent::Sync(files)
} else {
MemoryEvent::Update(files)
});
Ok(())
}
async fn remove_shadow_files(&self, files: MemoryFilesShort) -> Result<(), Error> {
// todo: is it safe to believe that the path is normalized?
let files = FileChangeSet::new_removes(files.files.into_iter().map(From::from).collect());
self.add_memory_changes(MemoryEvent::Update(files));
Ok(())
}
}
/// CLI Arguments for the preview tool.
#[derive(Debug, Clone, clap::Parser)]
pub struct PreviewCliArgs {
@ -218,11 +190,13 @@ pub struct PreviewState {
client: TypedLspClient<PreviewState>,
/// The backend running actor.
preview_tx: mpsc::UnboundedSender<PreviewRequest>,
/// the watchers for the preview
pub(crate) watchers: LspPreviewState,
}
impl PreviewState {
/// Create a new preview state.
pub fn new(client: TypedLspClient<PreviewState>) -> Self {
pub fn new(watchers: LspPreviewState, client: TypedLspClient<PreviewState>) -> Self {
let (preview_tx, preview_rx) = mpsc::unbounded_channel();
client.handle.spawn(
@ -230,11 +204,29 @@ impl PreviewState {
client: client.clone().to_untyped(),
tabs: HashMap::default(),
preview_rx,
watchers: watchers.clone(),
}
.run(),
);
Self { client, preview_tx }
Self {
client,
preview_tx,
watchers,
}
}
pub(crate) fn stop_all(&mut self) {
let watchers = std::mem::take(self.watchers.inner.lock().deref_mut());
for (_, watcher) in watchers {
let res = self.preview_tx.send(PreviewRequest::Kill(
watcher.task_id().to_owned(),
oneshot::channel().0,
));
if let Err(e) = res {
log::error!("failed to send kill request({:?}): {e}", watcher.task_id());
}
}
}
}
@ -248,15 +240,78 @@ pub struct StartPreviewResponse {
is_primary: bool,
}
pub struct PreviewProjectHandler {
pub project_id: ProjectInsId,
client: Box<dyn ProjectClient>,
}
impl PreviewProjectHandler {
pub fn flush_compile(&self) {
let _ = self.project_id;
self.client
.send_event(LspInterrupt::Compile(self.project_id.clone()));
}
pub async fn settle(&self) -> Result<(), Error> {
self.client
.send_event(LspInterrupt::Settle(self.project_id.clone()));
Ok(())
}
}
impl EditorServer for PreviewProjectHandler {
async fn update_memory_files(
&self,
files: MemoryFiles,
reset_shadow: bool,
) -> Result<(), Error> {
// todo: is it safe to believe that the path is normalized?
let files = FileChangeSet::new_inserts(
files
.files
.into_iter()
.map(|(path, content)| {
// todo: cloning PathBuf -> Arc<Path>
(path.into(), Ok(content.as_bytes().into()).into())
})
.collect(),
);
let intr = LspInterrupt::Memory(if reset_shadow {
MemoryEvent::Sync(files)
} else {
MemoryEvent::Update(files)
});
self.client.send_event(intr);
Ok(())
}
async fn remove_shadow_files(&self, files: MemoryFilesShort) -> Result<(), Error> {
// todo: is it safe to believe that the path is normalized?
let files = FileChangeSet::new_removes(files.files.into_iter().map(From::from).collect());
self.client
.send_event(LspInterrupt::Memory(MemoryEvent::Update(files)));
Ok(())
}
}
impl PreviewState {
/// Start a preview on a given compiler.
pub fn start(
&self,
args: PreviewCliArgs,
previewer: PreviewBuilder,
compile_handler: Arc<CompileHandler>,
// compile_handler: Arc<CompileHandler>,
project_id: ProjectInsId,
is_primary: bool,
) -> SchedulableResponse<StartPreviewResponse> {
let compile_handler = Arc::new(PreviewProjectHandler {
project_id,
client: Box::new(self.client.clone().to_untyped()),
});
let task_id = args.preview.task_id.clone();
log::info!("PreviewTask({task_id}): arguments: {args:#?}");
@ -502,6 +557,7 @@ pub async fn make_http_server(
/// Entry point of the preview tool.
pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
log::info!("Arguments: {args:#?}");
let handle = tokio::runtime::Handle::current();
let static_file_host =
if args.static_file_host == args.data_plane_host || !args.static_file_host.is_empty() {
@ -517,37 +573,62 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
});
let verse = args.compile.resolve()?;
let previewer = PreviewBuilder::new(args.preview);
let (service, handle) = {
// type EditorSender = mpsc::UnboundedSender<EditorRequest>;
let (editor_tx, mut editor_rx) = mpsc::unbounded_channel();
let (intr_tx, intr_rx) = mpsc::unbounded_channel();
let (intr_tx, intr_rx) = tokio::sync::mpsc::unbounded_channel();
let handle = Arc::new(CompileHandler {
inner: Default::default(),
diag_group: "main".to_owned(),
intr_tx: intr_tx.clone(),
// export_tx,
export: Default::default(),
editor_tx,
analysis: Arc::default(),
stats: Default::default(),
notified_revision: parking_lot::Mutex::new(0),
});
// todo: unify filesystem watcher
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
let fs_intr_tx = intr_tx.clone();
tokio::spawn(watch_deps(dep_rx, move |event| {
fs_intr_tx.send_event(LspInterrupt::Fs(event));
}));
// Consume editor_rx
tokio::spawn(async move { while editor_rx.recv().await.is_some() {} });
let service = CompileServerActor::new_with(
let preview_state = LspPreviewState::default();
// Create the actor
let compile_handle = Arc::new(CompileHandlerImpl {
preview: preview_state.clone(),
diag_group: "main".to_owned(),
export: crate::task::ExportTask::new(handle, Default::default()),
editor_tx,
client: Box::new(intr_tx.clone()),
analysis: Arc::default(),
notified_revision: parking_lot::Mutex::new(0),
});
let mut server = ProjectCompiler::new(
verse,
intr_tx,
intr_rx,
dep_tx,
CompileServerOpts {
compile_handle: handle.clone(),
handler: compile_handle,
enable_watch: true,
..Default::default()
},
)
.with_watch(true);
);
let registered = preview_state.register(&server.primary.id, previewer.compile_watcher());
if !registered {
anyhow::bail!("failed to register preview");
}
let handle = Arc::new(PreviewProjectHandler {
project_id: server.primary.id.clone(),
client: Box::new(intr_tx),
});
let service = async move {
let mut intr_rx = intr_rx;
while let Some(intr) = intr_rx.recv().await {
server.process(intr);
}
};
(service, handle)
};
@ -614,12 +695,9 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
let _ = srv.join.await;
});
let previewer = PreviewBuilder::new(args.preview);
let registered = handle.register_preview(previewer.compile_watcher());
assert!(registered, "failed to register preview");
let (websocket_tx, websocket_rx) = mpsc::unbounded_channel();
let mut previewer = previewer.build(lsp_tx, handle.clone()).await;
tokio::spawn(service.run());
tokio::spawn(service);
bind_streams(&mut previewer, websocket_rx);

View file

@ -47,7 +47,7 @@ macro_rules! get_arg_or_default {
}
pub(crate) use get_arg_or_default;
pub fn z_internal_error(msg: reflexo::Error) -> ResponseError {
pub fn z_internal_error(msg: tinymist_std::Error) -> ResponseError {
ResponseError {
code: ErrorCode::InternalError as i32,
message: format!("internal: {msg:?}"),