diff --git a/Cargo.lock b/Cargo.lock index 1f0f856ec..266eba694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4052,6 +4052,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "codespan-reporting", "comemo", "dirs", "flate2", @@ -4065,6 +4066,8 @@ dependencies = [ "serde_json", "tar", "tinymist-assets 0.12.18 (registry+https://github.com/rust-lang/crates.io-index)", + "tinymist-fs", + "tinymist-project", "tokio", "typst", "typst-assets", diff --git a/crates/sync-lsp/src/lib.rs b/crates/sync-lsp/src/lib.rs index af278b8f6..c81901b24 100644 --- a/crates/sync-lsp/src/lib.rs +++ b/crates/sync-lsp/src/lib.rs @@ -149,6 +149,19 @@ impl TypedLspClient { self.client .send_request_::(params, move |s, resp| handler(caster(s), resp)) } + + /// Sends a event to the client itself. + pub fn send_event(&self, event: T) { + let Some(sender) = self.sender.upgrade() else { + log::warn!("failed to send request: connection closed"); + return; + }; + + let Err(res) = sender.event.send(Box::new(event)) else { + return; + }; + log::warn!("failed to send event: {res:?}"); + } } impl Clone for TypedLspClient { diff --git a/crates/tinymist-world/Cargo.toml b/crates/tinymist-world/Cargo.toml index 194177473..b80879f16 100644 --- a/crates/tinymist-world/Cargo.toml +++ b/crates/tinymist-world/Cargo.toml @@ -26,8 +26,11 @@ rayon.workspace = true reflexo-typst.workspace = true reflexo-typst-shim = { workspace = true, features = ["nightly"] } typst.workspace = true +codespan-reporting = "0.11" tinymist-assets = { workspace = true } +tinymist-project.workspace = true +tinymist-fs.workspace = true typst-assets = { workspace = true, features = ["fonts"] } dirs.workspace = true diff --git a/crates/tinymist-world/src/lib.rs b/crates/tinymist-world/src/lib.rs index 5f387b7c1..192e4543d 100644 --- a/crates/tinymist-world/src/lib.rs +++ b/crates/tinymist-world/src/lib.rs @@ -1,6 +1,9 @@ //! World implementation of typst for tinymist. -use font::TinymistFontResolver; +pub mod font; +pub mod package; +pub mod project; + pub use reflexo_typst; pub use reflexo_typst::config::CompileFontOpts; pub use reflexo_typst::error::prelude; @@ -21,10 +24,9 @@ use reflexo_typst::vfs::{system::SystemAccessModel, Vfs}; use reflexo_typst::{CompilerFeat, CompilerUniverse, CompilerWorld, ImmutPath, TypstDict}; use serde::{Deserialize, Serialize}; -pub mod font; -pub mod package; -pub mod typ_server; -use package::HttpsRegistry; +use crate::font::TinymistFontResolver; +use crate::package::HttpsRegistry; +use crate::project::ProjectInterrupt; const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' }; @@ -201,6 +203,8 @@ pub type LspUniverse = TypstSystemUniverseExtend; pub type LspWorld = TypstSystemWorldExtend; /// Immutable prehashed reference to dictionary. pub type ImmutDict = Arc>; +/// LSP interrupt. +pub type LspInterrupt = ProjectInterrupt; /// Builder for LSP universe. pub struct LspUniverseBuilder; @@ -223,6 +227,18 @@ impl LspUniverseBuilder { )) } + /// Resolve fonts from given options. + pub fn only_embedded_fonts() -> ZResult { + let mut searcher = SystemFontSearcher::new(); + searcher.resolve_opts(CompileFontOpts { + font_profile_cache_path: Default::default(), + font_paths: vec![], + no_system_fonts: true, + with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(), + })?; + Ok(searcher.into()) + } + /// Resolve fonts from given options. pub fn resolve_fonts(args: CompileFontArgs) -> ZResult { let mut searcher = SystemFontSearcher::new(); diff --git a/crates/tinymist-world/src/typ_server.rs b/crates/tinymist-world/src/project.rs similarity index 71% rename from crates/tinymist-world/src/typ_server.rs rename to crates/tinymist-world/src/project.rs index 98064f94d..e7374bc33 100644 --- a/crates/tinymist-world/src/typ_server.rs +++ b/crates/tinymist-world/src/project.rs @@ -22,6 +22,9 @@ use reflexo_typst::{ }; use typst::diag::{SourceDiagnostic, SourceResult}; +mod lock; +pub use lock::*; + /// A signal that possibly triggers an export. /// /// Whether to export depends on the current state of the document and the user @@ -69,7 +72,7 @@ impl CompileSnapshot { self } - pub async fn compile(self) -> CompiledArtifact { + pub fn compile(self) -> CompiledArtifact { let mut snap = self; let warned = std::marker::PhantomData.compile(&snap.world, &mut snap.env); let (doc, warnings) = match warned { @@ -172,6 +175,8 @@ pub enum Interrupt { Compiled(CompiledArtifact), /// Change the watching entry. ChangeTask(TaskInputs), + /// Font changes. + Font(Arc), /// Request compiler to respond a snapshot without needing to wait latest /// compilation. SnapshotRead(oneshot::Sender>), @@ -193,23 +198,25 @@ enum CompilerResponse { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -struct CompileReasons { +pub struct CompileReasons { /// The snapshot is taken by the memory editing events. - by_memory_events: bool, + pub by_memory_events: bool, /// The snapshot is taken by the file system events. - by_fs_events: bool, + pub by_fs_events: bool, /// The snapshot is taken by the entry change. - by_entry_update: bool, + pub by_entry_update: bool, } impl CompileReasons { - fn see(&mut self, reason: 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; } - fn any(&self) -> bool { + /// Whether there is any reason to compile. + pub fn any(&self) -> bool { self.by_memory_events || self.by_fs_events || self.by_entry_update } } @@ -263,6 +270,329 @@ impl Default for CompileServerOpts { /// The compiler actor. pub struct CompileServerActor { + /// The wrapper + pub wrapper: CompilerServerWrapper, + + /// Channel for sending interrupts to the compiler actor. + intr_tx: mpsc::UnboundedSender>, + /// Channel for receiving interrupts from the compiler actor. + intr_rx: mpsc::UnboundedReceiver>, + + dep_tx: mpsc::UnboundedSender, + dep_rx: Option>, +} + +impl CompileServerActor { + /// Create a new compiler actor with options + pub fn new_with( + verse: CompilerUniverse, + intr_tx: mpsc::UnboundedSender>, + intr_rx: mpsc::UnboundedReceiver>, + opts: CompileServerOpts, + ) -> Self { + let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel(); + + Self { + wrapper: CompilerServerWrapper::new_with(verse, opts), + + intr_tx, + intr_rx, + dep_tx, + dep_rx: Some(dep_rx), + } + } + + /// Compile the document once. + async fn run_compile( + &mut self, + reason: CompileReasons, + curr_reads: &mut Vec>>, + is_once: bool, + ) -> Option> { + let compile = self.wrapper.run_compile(reason, curr_reads, is_once)?; + + if is_once { + Some(compile()) + } else { + let intr_tx = self.intr_tx.clone(); + tokio::task::spawn(async move { + let err = intr_tx.send(Interrupt::Compiled(compile())); + log_send_error("compiled", err); + }); + + None + } + } + + pub fn with_watch(mut self, watch: bool) -> Self { + self.wrapper.enable_watch = watch; + self + } + + /// Launches the compiler actor. + pub async fn run(mut self) -> bool { + // Trigger the first compilation (if active) + self.wrapper.start(); + + if !self.wrapper.enable_watch { + return true; + } + + // Spawn file system watcher. + let fs_tx = self.intr_tx.clone(); + let dep_rx = self.dep_rx.take().expect("start only once"); + tokio::spawn(watch_deps(dep_rx, move |event| { + log_send_error("fs_event", fs_tx.send(Interrupt::Fs(event))); + })); + + let dep_tx = self.dep_tx.clone(); + let mut curr_reads = vec![]; + + '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.wrapper.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.wrapper.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 + } +} + +pub trait ProjectHandle: Send + Sync + 'static { + fn on_any_compile_reason(&self, state: &mut ProjectCompiler); + fn notify_compile(&self, res: &CompiledArtifact, rep: CompileReport); + fn status(&self, revision: usize, rep: CompileReport); +} + +/// No need so no compilation. +impl ProjectHandle + for std::marker::PhantomData +{ + fn on_any_compile_reason(&self, _state: &mut ProjectCompiler) { + log::info!("ProjectHandle: no need to compile"); + } + fn notify_compile(&self, _res: &CompiledArtifact, _rep: CompileReport) {} + fn status(&self, _revision: usize, _rep: CompileReport) {} +} + +pub enum ProjectInterrupt { + /// Compile anyway. + Compile, + /// Compiled from computing thread. + Compiled(CompiledArtifact), + /// Change the watching entry. + ChangeTask(TaskInputs), + /// Font changes. + Font(Arc), + /// Memory file changes. + Memory(MemoryEvent), + /// File system event. + Fs(FilesystemEvent), +} + +pub struct ProjectState { + pub id: String, + /// The extension + pub ext: Ext, + /// The forked world. + pub world: CompilerWorld, + /// The reason to compile. + pub reason: CompileReasons, + + /// The latest compiled document. + pub(crate) latest_doc: Option>, + /// The latest successly compiled document. + latest_success_doc: Option>, +} + +pub struct ProjectCompiler { + pub wrapper: CompilerServerWrapper, + /// The customized compiler handle. + pub compile_handle: Arc>, + /// The primary compiler. + pub primary: ProjectState, + /// The compiler actors for tasks + pub dedicates: Vec>, + + /// Channel for sending interrupts to the compiler actor. + dep_tx: mpsc::UnboundedSender, +} + +impl ProjectCompiler { + /// Create a new compiler actor with options + pub fn new_with( + verse: CompilerUniverse, + dep_tx: mpsc::UnboundedSender, + opts: CompileServerOpts, + ) -> Self { + let wrapper = CompilerServerWrapper::new_with(verse, opts); + let primary = ProjectState { + id: "primary".to_string(), + ext: Default::default(), + world: wrapper.verse.snapshot(), + reason: no_reason(), + latest_doc: None, + latest_success_doc: None, + }; + let _ = primary.latest_doc; + Self { + wrapper, + compile_handle: Arc::new(std::marker::PhantomData), + primary, + dep_tx, + dedicates: vec![], + } + } + + // todo: options + pub fn with_compile_handle(mut self, handle: Arc>) -> Self { + self.compile_handle = handle; + self + } + + pub fn process(&mut self, intr: ProjectInterrupt) { + let intr = match intr { + ProjectInterrupt::Compile => Interrupt::Compile, + ProjectInterrupt::Compiled(compiled) => Interrupt::Compiled(compiled), + ProjectInterrupt::ChangeTask(task) => Interrupt::ChangeTask(task), + ProjectInterrupt::Font(font) => Interrupt::Font(font), + ProjectInterrupt::Memory(memory_event) => Interrupt::Memory(memory_event), + ProjectInterrupt::Fs(fs_event) => Interrupt::Fs(fs_event), + }; + + let reason = self.wrapper.process(intr, |resp| match resp { + CompilerResponse::Notify(msg) => { + log_send_error("notify", self.dep_tx.send(msg)); + } + }); + + self.primary.reason.see(reason); + for dedicate in &mut self.dedicates { + dedicate.reason.see(reason); + } + + // Customized Project Compilation Handler + self.compile_handle.clone().on_any_compile_reason(self); + + // if comp_reason.any() || !curr_reads.is_empty() { + // self.wrapper.run_compile(comp_reason, &mut curr_reads, + // false).await; } + } + + pub fn snapshot(&mut self) -> CompileSnapshot { + if self + .wrapper + .watch_snap + .get() + .is_some_and(|e| e.world.revision() < *self.wrapper.verse.revision.read()) + { + self.wrapper.watch_snap = OnceLock::new(); + } + + self.wrapper + .watch_snap + .get_or_init(|| self.wrapper.snapshot(false, no_reason())) + .clone() + } + + /// Compile the document once. + pub fn project_snapshot( + &self, + project: &ProjectState, + is_once: bool, + ) -> CompileSnapshot { + let world = self.wrapper.verse.snapshot(); + let env = self.wrapper.make_env(if is_once { + self.wrapper.once_feature_set.clone() + } else { + self.wrapper.watch_feature_set.clone() + }); + CompileSnapshot { + world, + env: env.clone(), + signal: ExportSignal { + by_entry_update: project.reason.by_entry_update, + by_mem_events: project.reason.by_memory_events, + by_fs_events: project.reason.by_fs_events, + }, + success_doc: project.latest_success_doc.clone(), + } + } + + /// Compile the document once. + pub fn run_compile( + h: Arc>, + snap: CompileSnapshot, + ) -> impl FnOnce() -> CompiledArtifact { + let start = reflexo_typst::time::now(); + + // todo unwrap main id + let id = snap.world.main_id().unwrap(); + let revision = snap.world.revision().get(); + + h.status(revision, 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), + }; + + 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 + } + } +} + +/// The compiler actor. +pub struct CompilerServerWrapper { /// The underlying universe. pub verse: CompilerUniverse, /// The compilation handle. @@ -286,11 +616,6 @@ pub struct CompileServerActor { /// Shared feature set for watch mode. watch_feature_set: Arc, - /// Channel for sending interrupts to the compiler actor. - intr_tx: mpsc::UnboundedSender>, - /// Channel for receiving interrupts from the compiler actor. - intr_rx: mpsc::UnboundedReceiver>, - watch_snap: OnceLock>, suspended: bool, compiling: bool, @@ -298,12 +623,10 @@ pub struct CompileServerActor { committed_revision: usize, } -impl CompileServerActor { +impl CompilerServerWrapper { /// Create a new compiler actor with options pub fn new_with( verse: CompilerUniverse, - intr_tx: mpsc::UnboundedSender>, - intr_rx: mpsc::UnboundedReceiver>, CompileServerOpts { compile_handle, feature_set, @@ -327,9 +650,6 @@ impl CompileServerActor { feature_set.configure(&WITH_COMPILING_STATUS_FEATURE, true), ), - intr_tx, - intr_rx, - watch_snap: OnceLock::new(), suspended: entry.is_inactive(), compiling: false, @@ -338,77 +658,20 @@ impl CompileServerActor { } } - pub fn with_watch(mut self, watch: bool) -> Self { - self.enable_watch = watch; - self - } - fn make_env(&self, feature_set: Arc) -> 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![]; - + pub fn start(&mut self) -> bool { 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; - } + let compile = self.run_compile(reason_by_entry_change(), &mut vec![], !self.enable_watch); + if let Some(compile) = compile { + compile(); } - log_send_error("settle_notify", dep_tx.send(NotifyMessage::Settle)); - log::info!("CompileServerActor: exited"); true } @@ -432,19 +695,18 @@ impl CompileServerActor { } /// Compile the document once. - pub async fn compile_once(&mut self) -> CompiledArtifact { + pub fn compile_once(&mut self) -> CompiledArtifact { self.run_compile(reason_by_entry_change(), &mut vec![], true) - .await - .expect("is_once is set") + .expect("is_once is set")() } /// Compile the document once. - async fn run_compile( + fn run_compile( &mut self, reason: CompileReasons, curr_reads: &mut Vec>>, is_once: bool, - ) -> Option> { + ) -> Option CompiledArtifact> { self.suspended_reason.see(reason); let reason = std::mem::take(&mut self.suspended_reason); let start = reflexo_typst::time::now(); @@ -478,8 +740,8 @@ impl CompileServerActor { h.status(revision, CompileReport::Stage(id, "compiling", start)); - let compile = move || async move { - let compiled = compiling.compile().await; + Some(move || { + let compiled = compiling.compile(); for reader in curr_reads { let _ = reader.send(SucceededArtifact::Compiled(compiled.clone())); @@ -500,19 +762,7 @@ impl CompileServerActor { 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, send: impl Fn(CompilerResponse)) { @@ -559,6 +809,13 @@ impl CompileServerActor { reason_by_entry_change() } + Interrupt::Font(font) => { + self.verse.increment_revision(|verse| { + verse.inner.font_resolver = font; + }); + + reason_by_entry_change() + } Interrupt::SnapshotRead(task) => { log::debug!("CompileServerActor: take snapshot"); if self diff --git a/crates/tinymist-world/src/project/lock.rs b/crates/tinymist-world/src/project/lock.rs new file mode 100644 index 000000000..b24694f2c --- /dev/null +++ b/crates/tinymist-world/src/project/lock.rs @@ -0,0 +1,155 @@ +use std::{path::Path, sync::Arc}; + +use tinymist_project::{ + Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath, +}; + +use reflexo_typst::{path::unix_slash, EntryReader, TypstFileId}; +use typst::diag::EcoString; + +use crate::LspWorld; + +/// Make a new project lock updater. +pub fn update_lock(world: &LspWorld) -> Option { + let root = world.workspace_root()?; + Some(ProjectLockUpdater { + root, + updates: vec![], + }) +} + +enum LockUpdate { + Input(ProjectInput), + Task(ProjectTask), + Material(ProjectMaterial), + Route(ProjectRoute), +} + +pub struct ProjectLockUpdater { + root: Arc, + updates: Vec, +} + +impl ProjectLockUpdater { + pub fn compiled(&mut self, world: &LspWorld) -> Option { + let entry = world.entry_state(); + log::info!("ProjectCompiler: record compile for {entry:?}"); + // todo: correct root + let root = entry.workspace_root()?; + let id = unix_slash(entry.main()?.vpath().as_rootless_path()); + log::info!("ProjectCompiler: record compile for id {id} at {root:?}"); + + let path = &ResourcePath::from_user_sys(Path::new(&id)); + let id: Id = path.into(); + + let root = ResourcePath::from_user_sys(Path::new(".")); + + let font_resolver = &world.font_resolver; + let font_paths = font_resolver + .font_paths() + .iter() + .map(|p| ResourcePath::from_user_sys(p)) + .collect::>(); + + // let system_font = font_resolver.system_font(); + + let registry = &world.registry; + let package_path = registry + .package_path() + .map(|p| ResourcePath::from_user_sys(p)); + let package_cache_path = registry + .package_cache_path() + .map(|p| ResourcePath::from_user_sys(p)); + + // todo: freeze the package paths + let _ = package_cache_path; + let _ = package_path; + + let input = ProjectInput { + id: id.clone(), + root: Some(root), + font_paths, + system_fonts: true, // !args.font.ignore_system_fonts, + package_path: None, + package_cache_path: None, + }; + + self.updates.push(LockUpdate::Input(input)); + + Some(id) + } + + pub fn task(&mut self, task: ProjectTask) { + self.updates.push(LockUpdate::Task(task)); + } + + pub fn update_materials(&mut self, doc_id: Id, ids: Vec) { + let mut files = ids + .into_iter() + .map(ResourcePath::from_file_id) + .collect::>(); + files.sort(); + self.updates.push(LockUpdate::Material(ProjectMaterial { + root: EcoString::default(), + id: doc_id, + files, + })); + } + + pub fn route(&mut self, doc_id: Id, priority: u32) { + self.updates.push(LockUpdate::Route(ProjectRoute { + id: doc_id, + priority, + })); + } + + pub fn commit(self) { + let err = tinymist_project::LockFile::update(&self.root, |l| { + let root: EcoString = unix_slash(&self.root).into(); + let root_hash = reflexo_typst::hash::hash128(&root); + for update in self.updates { + match update { + LockUpdate::Input(input) => { + l.replace_document(input); + } + LockUpdate::Task(task) => { + l.replace_task(task); + } + LockUpdate::Material(mut mat) => { + mat.root = root.clone(); + let cache_dir = dirs::cache_dir(); + if let Some(cache_dir) = cache_dir { + let id = reflexo_typst::hash::hash128(&mat.id); + let lower4096 = root_hash & 0xfff; + let upper4096 = root_hash >> 12; + + // let hash_str = format!("{root:016x}/{id:016x}"); + let hash_str = format!("{lower4096:03x}/{upper4096:013x}/{id:016x}"); + + let cache_dir = cache_dir.join("tinymist/projects").join(hash_str); + let _ = std::fs::create_dir_all(&cache_dir); + + let data = serde_json::to_string(&mat).unwrap(); + let path = cache_dir.join("material.json"); + let result = tinymist_fs::paths::write_atomic(path, data); + if let Err(err) = result { + log::error!("ProjectCompiler: write material error: {err}"); + } + + // todo: clean up old cache + } + // l.replace_material(mat); + } + LockUpdate::Route(route) => { + l.replace_route(route); + } + } + } + + Ok(()) + }); + if let Err(err) = err { + log::error!("ProjectCompiler: lock file error: {err}"); + } + } +} diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index d01e9b6e8..2570b33c2 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -8,21 +8,22 @@ pub mod typ_client; use std::sync::Arc; use reflexo::ImmutPath; -use reflexo_typst::vfs::notify::{FileChangeSet, MemoryEvent}; +use reflexo_typst::watch_deps; use reflexo_typst::world::EntryState; use tinymist_query::analysis::{Analysis, PeriscopeProvider}; use tinymist_query::{ExportKind, LocalContext, VersionedDocument}; use tinymist_render::PeriscopeRenderer; -use tokio::sync::mpsc; +use tinymist_world::LspInterrupt; use typst::layout::Position; +use crate::stats::CompilerQueryStats; use crate::{ task::{ExportConfig, ExportTask, ExportUserConfig}, world::{ImmutDict, LspUniverseBuilder}, LanguageState, }; -use tinymist_world::typ_server::{CompileServerActor, CompileServerOpts}; -use typ_client::{CompileClientActor, CompileHandler}; +use tinymist_world::typ_server::{CompileServerOpts, ProjectCompiler}; +use typ_client::{CompileClientActor, LocalCompileHandler, LspProjectHandler}; impl LanguageState { /// Restart the primary server. @@ -42,7 +43,6 @@ impl LanguageState { group.to_owned(), self.entry_resolver().resolve(entry), self.compile_config().determine_inputs(), - self.vfs_snapshot(), ); let prev = if group == "primary" { @@ -68,16 +68,13 @@ impl LanguageState { /// Create a new server for the given group. pub fn server( &self, - editor_group: String, + diag_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(), + group: diag_group.clone(), editor_tx: Some(self.editor_tx.clone()), config: ExportUserConfig { output: self.compile_config().output_path.clone(), @@ -90,20 +87,19 @@ impl LanguageState { }); log::info!( - "TypstActor: creating server for {editor_group}, entry: {entry:?}, inputs: {inputs:?}" + "TypstActor: creating server for {diag_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 { + let handle = Arc::new(LspProjectHandler { #[cfg(feature = "preview")] inner: std::sync::Arc::new(parking_lot::RwLock::new(None)), - diag_group: editor_group.clone(), - intr_tx: intr_tx.clone(), + diag_group: diag_group.clone(), export: export.clone(), editor_tx: self.editor_tx.clone(), - stats: Default::default(), + client: self.client.clone().to_untyped(), analysis: Arc::new(Analysis { position_encoding: const_config.position_encoding, allow_overlapping_token: const_config.tokens_overlapping_token_support, @@ -134,37 +130,49 @@ impl LanguageState { let cert_path = self.compile_config().determine_certification_path(); let package = self.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 client = self.client.clone().to_untyped(); + let async_handle = client.handle.clone(); + async_handle.spawn(watch_deps(dep_rx, move |event| { + client.send_event(LspInterrupt::Fs(event)); + })); + + // Create the actor + let server = ProjectCompiler::new_with(verse, dep_tx, CompileServerOpts::default()) + .with_compile_handle(compile_handle); + let client = self.client.clone(); 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, - ..Default::default() - }, - ) - .with_watch(true); - tokio::spawn(server.run()); + client.send_event(LspInterrupt::Font(font_resolver)); }); + let handle = LocalCompileHandler { + diag_group, + wrapper: server, + analysis: handle.analysis.clone(), + stats: CompilerQueryStats::default(), + export: handle.export.clone(), + }; + // Create the client let config = self.compile_config().clone(); - let client = CompileClientActor::new(handle, config, entry); + + // 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)); - client + // client.add_memory_changes(MemoryEvent::Update(snapshot)); + CompileClientActor::new(handle, config, entry) } } diff --git a/crates/tinymist/src/actor/preview.rs b/crates/tinymist/src/actor/preview.rs index ba535bcb2..7933f1d62 100644 --- a/crates/tinymist/src/actor/preview.rs +++ b/crates/tinymist/src/actor/preview.rs @@ -7,8 +7,7 @@ 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::tool::preview::{HttpServer, PreviewProjectHandler}; pub struct PreviewTab { /// Task ID @@ -20,7 +19,7 @@ pub struct PreviewTab { /// Control plane message sender pub ctl_tx: mpsc::UnboundedSender, /// Compile handler - pub compile_handler: Arc, + pub compile_handler: Arc, /// Whether this tab is primary pub is_primary: bool, } diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs index 2ef950534..1b610d7dd 100644 --- a/crates/tinymist/src/actor/typ_client.rs +++ b/crates/tinymist/src/actor/typ_client.rs @@ -29,16 +29,21 @@ use log::{error, info, trace}; use reflexo::path::unix_slash; use reflexo_typst::{ error::prelude::*, typst::prelude::*, vfs::notify::MemoryEvent, world::EntryState, - CompileReport, EntryReader, Error, ImmutPath, TaskInputs, + CompileReport, Compiler, EntryReader, Error, ImmutPath, TaskInputs, }; -use sync_lsp::just_future; +use sync_lsp::{just_future, LspClient}; +use tinymist_project::PROJECT_ROUTE_USER_ACTION_PRIORITY; use tinymist_query::{ analysis::{Analysis, AnalysisRevLock, LocalContextGuard}, CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, EntryResolver, OnExportRequest, SemanticRequest, ServerInfoResponse, StatefulRequest, VersionedDocument, }; -use tinymist_world::typ_server::{ - CompilationHandle, CompileSnapshot, CompiledArtifact, Interrupt, SucceededArtifact, +use tinymist_world::{ + project::{ + update_lock, CompilationHandle, CompileSnapshot, CompiledArtifact, Interrupt, + ProjectCompiler, ProjectHandle, ProjectInterrupt, + }, + LspInterrupt, }; use tokio::sync::{mpsc, oneshot}; use typst::{diag::SourceDiagnostic, World}; @@ -53,34 +58,59 @@ use crate::{ type EditorSender = mpsc::UnboundedSender; -pub struct CompileHandler { - pub(crate) diag_group: String, - pub(crate) analysis: Arc, - pub(crate) stats: CompilerQueryStats, - +#[derive(Default)] +pub struct ProjectExt { #[cfg(feature = "preview")] pub(crate) inner: Arc>>>, +} - pub(crate) intr_tx: mpsc::UnboundedSender>, - pub(crate) export: ExportTask, - pub(crate) editor_tx: EditorSender, +#[cfg(feature = "preview")] +impl ProjectExt { + // todo: multiple preview support + #[must_use] + pub fn register_preview(&self, handle: &Arc) -> bool { + let mut p = self.inner.write(); + if p.as_ref().is_some() { + return false; + } + *p = Some(handle.clone()); + true + } - pub(crate) notified_revision: parking_lot::Mutex, + #[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 CompileHandler { +/// LSP project compiler. +pub type LspProjectCompiler = ProjectCompiler; + +pub struct LocalCompileHandler { + pub(crate) diag_group: String, + pub(crate) wrapper: LspProjectCompiler, + pub(crate) analysis: Arc, + pub(crate) stats: CompilerQueryStats, + pub(crate) export: ExportTask, +} + +impl LocalCompileHandler { /// Snapshot the compiler thread for tasks - pub fn snapshot(&self) -> ZResult { + pub fn snapshot(&mut self) -> ZResult { let (tx, rx) = oneshot::channel(); - self.intr_tx - .send(Interrupt::SnapshotRead(tx)) - .map_err(map_string_err("failed to send snapshot request"))?; + let snap = self.wrapper.snapshot(); + let _ = tx.send(snap); Ok(WorldSnapFut { rx }) } /// Snapshot the compiler thread for language queries - pub fn query_snapshot(&self, q: Option<&CompilerQueryRequest>) -> ZResult { + pub fn query_snapshot(&mut self, q: Option<&CompilerQueryRequest>) -> ZResult { let fut = self.snapshot()?; let analysis = self.analysis.clone(); let rev_lock = analysis.lock_revision(q); @@ -92,34 +122,206 @@ impl CompileHandler { }) } - /// Get latest artifact the compiler thread for tasks - pub fn artifact(&self) -> ZResult { - 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(&mut self) { + // todo: better way to flush compile + self.wrapper.process(ProjectInterrupt::Compile); } - pub fn flush_compile(&self) { - // todo: better way to flush compile - let _ = self.intr_tx.send(Interrupt::Compile); + pub fn add_memory_changes(&mut self, event: MemoryEvent) { + self.wrapper.process(ProjectInterrupt::Memory(event)); } - pub fn add_memory_changes(&self, event: MemoryEvent) { - let _ = self.intr_tx.send(Interrupt::Memory(event)); + pub fn interrupt(&mut self, intr: ProjectInterrupt) { + self.wrapper.process(intr); } - pub fn change_task(&self, task_inputs: TaskInputs) { - let _ = self.intr_tx.send(Interrupt::ChangeTask(task_inputs)); + pub fn change_task(&mut self, task: TaskInputs) { + self.wrapper.process(ProjectInterrupt::ChangeTask(task)); } pub async fn settle(&self) -> anyhow::Result<()> { - let (tx, rx) = oneshot::channel(); - let _ = self.intr_tx.send(Interrupt::Settle(tx)); - rx.await?; - Ok(()) + // let (tx, rx) = oneshot::channel(); + // let _ = self.intr_tx.send(Interrupt::Settle(tx)); + // rx.await?; + // Ok(()) + todo!() + } +} + +pub struct LspProjectHandler { + pub(crate) diag_group: String, + pub(crate) analysis: Arc, + + #[cfg(feature = "preview")] + pub(crate) inner: Arc>>>, + + pub(crate) export: ExportTask, + pub(crate) editor_tx: EditorSender, + pub(crate) client: LspClient, + + pub(crate) notified_revision: parking_lot::Mutex, +} + +impl LspProjectHandler { + fn push_diagnostics(&self, revision: usize, diagnostics: Option) { + 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, + warnings: EcoVec, + ) { + 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 ProjectHandle for LspProjectHandler { + fn on_any_compile_reason(&self, c: &mut LspProjectCompiler) { + let instances = std::iter::once(&c.primary).chain(c.dedicates.iter()); + for s in instances { + let snap = c.project_snapshot(s, false); + + let compile_fn = ProjectCompiler::run_compile(c.compile_handle.clone(), snap); + rayon::spawn(|| { + compile_fn(); + }); + } + + let instances_mut = std::iter::once(&mut c.primary).chain(c.dedicates.iter_mut()); + for s in instances_mut { + s.reason = Default::default(); + } + } + + 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, 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.inner.read().as_ref() { + let snap = snap.clone(); + inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { snap })); + } + } +} + +pub struct CompileHandler { + pub(crate) diag_group: String, + pub(crate) analysis: Arc, + + #[cfg(feature = "preview")] + pub(crate) inner: Arc>>>, + + pub(crate) intr_tx: mpsc::UnboundedSender>, + pub(crate) export: ExportTask, + pub(crate) editor_tx: EditorSender, + + pub(crate) notified_revision: parking_lot::Mutex, +} + +impl CompileHandler { + pub fn add_memory_changes(&self, event: MemoryEvent) { + let _ = self.intr_tx.send(Interrupt::Memory(event)); } fn push_diagnostics(&self, revision: usize, diagnostics: Option) { @@ -167,17 +369,6 @@ impl CompileHandler { *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 for CompileHandler { @@ -270,7 +461,7 @@ impl CompilationHandle for CompileHandler { } pub struct CompileClientActor { - pub handle: Arc, + pub handle: LocalCompileHandler, pub config: CompileConfig, entry: EntryState, @@ -278,7 +469,7 @@ pub struct CompileClientActor { impl CompileClientActor { pub(crate) fn new( - handle: Arc, + handle: LocalCompileHandler, config: CompileConfig, entry: EntryState, ) -> Self { @@ -290,8 +481,8 @@ impl CompileClientActor { } /// Snapshot the compiler thread for tasks - pub fn snapshot(&self) -> ZResult { - self.handle.clone().snapshot() + pub fn snapshot(&mut self) -> ZResult { + self.handle.snapshot() } /// Get the entry resolver. @@ -300,24 +491,31 @@ impl CompileClientActor { } /// Snapshot the compiler thread for language queries - pub fn query_snapshot(&self) -> ZResult { - self.handle.clone().query_snapshot(None) + pub fn query_snapshot(&mut self) -> ZResult { + self.handle.query_snapshot(None) } /// Snapshot the compiler thread for language queries - pub fn query_snapshot_with_stat(&self, q: &CompilerQueryRequest) -> ZResult { + pub fn query_snapshot_with_stat( + &mut self, + q: &CompilerQueryRequest, + ) -> ZResult { 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))?; + let fut = self.handle.query_snapshot(Some(q))?; Ok(QuerySnapWithStat { fut, stat }) } - pub fn add_memory_changes(&self, event: MemoryEvent) { + pub fn add_memory_changes(&mut self, event: MemoryEvent) { self.handle.add_memory_changes(event); } - pub fn change_task(&self, task_inputs: TaskInputs) { + pub fn interrupt(&mut self, intr: LspInterrupt) { + self.handle.interrupt(intr); + } + + pub fn change_task(&mut self, task_inputs: TaskInputs) { self.handle.change_task(task_inputs); } @@ -329,13 +527,45 @@ impl CompileClientActor { self.handle.export.change_config(config); } - pub fn on_export(&self, req: OnExportRequest) -> QueryFuture { + pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture { let OnExportRequest { path, kind, open } = req; - let snap = self.snapshot()?; + let snap = self.query_snapshot()?; + + let update_dep = async move { + let mut snap = snap.receive().await?; + let world = snap.world.clone(); + let updater = update_lock(&world); + + // todo: Introducing additional compilation here, but we must get accurate + // dependencies by compiling it again. + snap.snap.world.source_db.take_state(); + let err = std::marker::PhantomData.compile(&snap.world, &mut Default::default()); + if err.is_err() { + return ZResult::Ok(()); + } + + let ids = snap.run_analysis(|lg| lg.depended_files()).ok(); + + let _ = updater.zip(ids).and_then(|(mut updater, file_ids)| { + let doc_id = updater.compiled(&world)?; + + updater.update_materials(doc_id.clone(), file_ids); + updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY); + + updater.commit(); + + Some(()) + }); + + ZResult::Ok(()) + }; + + 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 { + tokio::spawn(update_dep); let res = export.await?; // See https://github.com/Myriad-Dreamin/tinymist/issues/837 @@ -403,7 +633,7 @@ impl CompileClientActor { self.handle.analysis.clear_cache(); } - pub fn collect_server_info(&self) -> QueryFuture { + pub fn collect_server_info(&mut 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(); @@ -523,16 +753,3 @@ impl QuerySnap { Ok(f(&mut analysis)) } } - -pub struct ArtifactSnap { - rx: oneshot::Receiver>, -} - -impl ArtifactSnap { - /// Get latest artifact the compiler thread for tasks - pub async fn receive(self) -> ZResult> { - self.rx - .await - .map_err(map_string_err("failed to get snapshot")) - } -} diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index 4d53d0a3f..4d972f08e 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -211,9 +211,7 @@ impl LanguageState { /// Clear all cached resources. pub fn clear_cache(&mut self, _arguments: Vec) -> AnySchedulableResponse { comemo::evict(0); - for dead in self.servers_mut() { - dead.clear_cache(); - } + self.primary().clear_cache(); just_ok(JsonValue::Null) } @@ -255,6 +253,7 @@ impl LanguageState { use std::path::Path; use crate::tool::preview::PreviewCliArgs; + use actor::typ_client::ProjectExt; use clap::Parser; let cli_args = get_arg_or_default!(args[0] as Vec); @@ -286,30 +285,44 @@ impl LanguageState { let previewer = typst_preview::PreviewBuilder::new(cli_args.preview.clone()); - let primary = self.primary().handle.clone(); - if !cli_args.not_as_primary && primary.register_preview(previewer.compile_watcher()) { + let handle = &mut self.primary().handle; + let primary = &mut handle.wrapper.primary; + let _ = ProjectExt::unregister_preview; + if !cli_args.not_as_primary && primary.ext.register_preview(previewer.compile_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 { - 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()) { - return Err(invalid_params( - "cannot register preview to the compiler instance", - )); - } - - self.preview.start(cli_args, previewer, handle, false) + // Get the task-dedicated compile server. + // pub fn dedicate(&self, group: &str) -> Option<&CompileClientActor> { + // self.dedicates + // .iter() + // .find(|dedicate| dedicate.handle.diag_group == group) + // } + + // self.restart_dedicate(&task_id, Some(entry)); + // let Some(dedicate) = self.dedicate(&task_id) else { + // return Err(invalid_params( + // "just restarted compiler instance for the task is not found", + // )); + // }; + + // let _ = dedicate; + + // let handle = dedicate.handle.clone(); + + // if !handle.register_preview(previewer.compile_watcher()) { + // return Err(invalid_params( + // "cannot register preview to the compiler instance", + // )); + // } + + // self.preview.start(cli_args, previewer, handle, false) + + todo!() } } diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 7af1c4360..4f4ecb8be 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -28,7 +28,6 @@ use tinymist_query::{ }; use tinymist_query::{EntryResolver, PageSelection}; use tokio::sync::mpsc; -use typ_server::Interrupt; use typst::{diag::FileResult, syntax::Source}; use super::{init::*, *}; @@ -167,20 +166,8 @@ impl LanguageState { } /// 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 { - self.primary.iter_mut().chain(self.dedicates.iter_mut()) + pub fn primary(&mut self) -> &mut CompileClientActor { + self.primary.as_mut().expect("primary") } /// Install handlers to the language server. @@ -201,7 +188,7 @@ impl LanguageState { let mut provider = provider .with_request::(State::shutdown) // customized event - .with_event(&Interrupt::Compile, State::compile_interrupt::) + .with_event(&LspInterrupt::Compile, State::compile_interrupt::) // lantency sensitive .with_request_::(State::completion) .with_request_::(State::semantic_tokens_full) @@ -278,30 +265,17 @@ 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((meta.mt, content)).into()) - }) - .collect(), - ) - } - fn compile_interrupt>( mut state: ServiceState, - _params: Interrupt, + params: LspInterrupt, ) -> anyhow::Result<()> { let Some(ready) = state.ready() else { log::info!("interrupted on not ready server"); return Ok(()); }; - let _ = ready; - log::info!("interrupted"); + let server = ready.primary(); + server.interrupt(params); Ok(()) } } @@ -928,9 +902,8 @@ impl LanguageState { 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.primary() + .add_memory_changes(MemoryEvent::Update(files.clone())); Ok(()) } @@ -940,6 +913,7 @@ impl LanguageState { let now = Time::now(); let path: ImmutPath = path.into(); + log::info!("create source: {path:?}"); self.memory_changes.insert( path.clone(), MemoryFileMeta { @@ -949,7 +923,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((now, content)).into())]); @@ -1038,7 +1011,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)?, @@ -1046,14 +1018,14 @@ 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.primary().on_export(req), + ServerInfo(_) => return self.primary().collect_server_info(), + _ => return Self::query_on(self.primary(), is_pinning, query), }) } fn query_on( - client: &CompileClientActor, + client: &mut CompileClientActor, is_pinning: bool, query: CompilerQueryRequest, ) -> QueryFuture { diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index 5b3852dc3..095e1a7db 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -5,8 +5,12 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::{bail, Context}; use reflexo_typst::{EntryReader, EntryState, TaskInputs, TypstDatetime}; +use tinymist_project::{ + ExportHtmlTask, ExportMarkdownTask, ExportPdfTask, ExportPngTask, ExportTextTask, ProjectTask, + TaskWhen, +}; use tinymist_query::{ExportKind, PageSelection}; -use tinymist_world::typ_server::{CompiledArtifact, ExportSignal}; +use tinymist_world::typ_server::{update_lock, CompiledArtifact, ExportSignal}; use tokio::sync::mpsc; use typlite::Typlite; use typst::foundations::IntoValue; @@ -76,8 +80,8 @@ impl SyncTaskFactory { ..Default::default() }); - let artifact = snap.compile().await; - export.do_export(&kind, artifact).await + let artifact = snap.compile(); + export.do_export(&kind, artifact, false).await } } } @@ -127,7 +131,7 @@ impl ExportConfig { let this = self.clone(); let artifact = artifact.clone(); Box::pin(async move { - log_err(this.do_export(&this.kind, artifact).await); + log_err(this.do_export(&this.kind, artifact, true).await); Some(()) }) }); @@ -167,6 +171,7 @@ impl ExportConfig { &self, kind: &ExportKind, artifact: CompiledArtifact, + passive: bool, ) -> anyhow::Result> { use reflexo_vec2svg::DefaultExportFeature; use ExportKind::*; @@ -195,6 +200,70 @@ impl ExportConfig { } } + if !passive { + let updater = update_lock(&snap.world); + + let _ = updater.and_then(|mut updater| { + let doc_id = updater.compiled(&snap.world)?; + let task_id = doc_id.clone(); + + let when = match self.config.mode { + ExportMode::Never => TaskWhen::Never, + ExportMode::OnType => TaskWhen::OnType, + ExportMode::OnSave => TaskWhen::OnSave, + ExportMode::OnDocumentHasTitle => TaskWhen::OnSave, + }; + + // todo: page transforms + let transforms = vec![]; + + use tinymist_project::ExportTask as ProjectExportTask; + + let export = ProjectExportTask { + document: doc_id, + id: task_id, + when, + transform: transforms, + }; + + let task = match kind { + Pdf { creation_timestamp } => { + let _ = creation_timestamp; + ProjectTask::ExportPdf(ExportPdfTask { + export, + pdf_standards: Default::default(), + }) + } + Html {} => ProjectTask::ExportHtml(ExportHtmlTask { export }), + Markdown {} => ProjectTask::ExportMarkdown(ExportMarkdownTask { export }), + Text {} => ProjectTask::ExportText(ExportTextTask { export }), + Query { .. } => { + // todo: ignoring query task. + return None; + } + Svg { page } => { + // todo: ignoring page selection. + let _ = page; + return None; + } + Png { ppi, fill, page } => { + // todo: ignoring page fill. + let _ = fill; + // todo: ignoring page selection. + let _ = page; + + let ppi = ppi.unwrap_or(144.) as f32; + ProjectTask::ExportPng(ExportPngTask { export, ppi }) + } + }; + + updater.task(task); + updater.commit(); + + Some(()) + }); + } + // Prepare the document. let doc = doc.map_err(|_| anyhow::anyhow!("no document"))?; diff --git a/crates/tinymist/src/tool/preview.rs b/crates/tinymist/src/tool/preview.rs index 5dedeea5a..cfe086e35 100644 --- a/crates/tinymist/src/tool/preview.rs +++ b/crates/tinymist/src/tool/preview.rs @@ -1,5 +1,7 @@ //! Document preview tool for Typst +#![allow(missing_docs)] + use std::num::NonZeroUsize; use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc}; @@ -257,15 +259,87 @@ pub struct StartPreviewResponse { is_primary: bool, } +pub struct PreviewProjectHandler { + client: TypedLspClient, + project_id: String, +} + +impl PreviewProjectHandler { + pub fn flush_compile(&self) { + let _ = self.project_id; + self.client.send_event(LspInterrupt::Compile); + } + + pub async fn settle(&self) -> Result<(), Error> { + // let req = PreviewRequest::Kill(task_id.to_owned(), oneshot::channel().1); + // let _ = self.client.send_event(req); + // true + todo!() + } + + pub fn unregister_preview(&self, _task_id: &str) -> bool { + // let req = PreviewRequest::Kill(task_id.to_owned(), oneshot::channel().1); + // let _ = self.client.send_event(req); + // true + todo!() + } +} + +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 now = std::time::SystemTime::now(); + // let files = FileChangeSet::new_inserts( + // files + // .files + // .into_iter() + // .map(|(path, content)| { + // let content = content.as_bytes().into(); + // // todo: cloning PathBuf -> Arc + // (path.into(), Ok((now, content)).into()) + // }) + // .collect(), + // ); + // self.add_memory_changes(if reset_shadow { + // MemoryEvent::Sync(files) + // } else { + // MemoryEvent::Update(files) + // }); + + // Ok(()) + todo!() + } + + 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(()) + todo!() + } +} + impl PreviewState { /// Start a preview on a given compiler. pub fn start( &self, args: PreviewCliArgs, previewer: PreviewBuilder, - compile_handler: Arc, + // compile_handler: Arc, + project_id: String, is_primary: bool, ) -> SchedulableResponse { + let compile_handler = Arc::new(PreviewProjectHandler { + project_id, + client: self.client.clone(), + }); + let task_id = args.preview.task_id.clone(); log::info!("PreviewTask({task_id}): arguments: {args:#?}"); @@ -540,7 +614,6 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> { export: Default::default(), editor_tx, analysis: Arc::default(), - stats: Default::default(), notified_revision: parking_lot::Mutex::new(0), }); diff --git a/editors/vscode/Configuration.md b/editors/vscode/Configuration.md index 65dec99c3..9b2c2c621 100644 --- a/editors/vscode/Configuration.md +++ b/editors/vscode/Configuration.md @@ -1,5 +1,15 @@ # Tinymist Server Configuration +## `tinymist.projectResolution` + +This configuration specifies the way to resolved projects. + +- **Type**: `string` +- **Enum**: + - `singleFile`: Manage typst documents like what we did in Markdown. Each single file is an individual document and no project resolution is needed. + - `lockDatabase`: Manage typst documents like what we did in Rust. For each workspace, tinymist tracks your preview and compilation history, and stores the information in a lock file. Tinymist will automatically selects the main file to use according to the lock file. This also allows other tools push preview and export tasks to language server by updating the lock file. +- **Default**: `"singleFile"` + ## `tinymist.outputPath` The path pattern to store Typst artifacts, you can use `$root` or `$dir` or `$name` to do magic configuration, e.g. `$dir/$name` (default) and `$root/target/$dir/$name`.