From cda7a4f891ba970680da564c870f3d198fa9ff0d Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Fri, 17 Jan 2025 16:01:46 +0800 Subject: [PATCH] feat: remove typ_client --- crates/tinymist/src/actor/mod.rs | 193 -------- crates/tinymist/src/cmd.rs | 26 +- crates/tinymist/src/lib.rs | 4 +- crates/tinymist/src/main.rs | 4 +- .../src/{actor/typ_client.rs => project.rs} | 245 +--------- crates/tinymist/src/resource/fonts.rs | 2 +- crates/tinymist/src/resource/symbols.rs | 2 +- crates/tinymist/src/server.rs | 434 ++++++++++++++++-- crates/tinymist/src/task/export.rs | 4 +- crates/tinymist/src/task/user_action.rs | 2 +- crates/tinymist/src/tool/preview.rs | 5 +- 11 files changed, 426 insertions(+), 495 deletions(-) rename crates/tinymist/src/{actor/typ_client.rs => project.rs} (60%) diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index 777f27df1..a2acdbe8c 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -3,196 +3,3 @@ pub mod editor; #[cfg(feature = "preview")] pub mod preview; -pub mod typ_client; - -use std::sync::Arc; - -use reflexo::ImmutPath; -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 tinymist_world::LspInterrupt; -use typst::layout::Position; - -use crate::stats::CompilerQueryStats; -use crate::{ - task::{ExportConfig, ExportTask, ExportUserConfig}, - world::{ImmutDict, LspUniverseBuilder}, - LanguageState, -}; -use tinymist_world::project::{CompileServerOpts, ProjectCompiler}; -use typ_client::{CompileClientActor, LocalCompileHandler, LspProjectHandler}; - -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) { - self.restart_server(dedicate, entry); - } - - /// Restart the server with the given group. - fn restart_server(&mut self, group: &str, entry: Option) { - let server = self.server( - group.to_owned(), - self.entry_resolver().resolve(entry), - self.compile_config().determine_inputs(), - ); - - 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, - diag_group: String, - entry: EntryState, - inputs: ImmutDict, - ) -> CompileClientActor { - // Run Export actors before preparing cluster to avoid loss of events - let config = ExportConfig { - group: diag_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, - }; - let export = ExportTask::new(self.client.handle.clone(), config); - - log::info!( - "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(LspProjectHandler { - #[cfg(feature = "preview")] - inner: std::sync::Arc::new(parking_lot::RwLock::new(None)), - diag_group: diag_group.clone(), - export: export.clone(), - editor_tx: self.editor_tx.clone(), - client: Box::new(self.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: !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 - }), - 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 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 { - compile_handle, - ..Default::default() - }, - ); - let client = self.client.clone(); - self.client.handle.spawn_blocking(move || { - // Create the world - let font_resolver = font_resolver.wait().clone(); - 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(); - - // 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)); - CompileClientActor::new(handle, config, entry) - } -} - -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 { - self.0.render_marked(ctx, doc, pos) - } -} diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index 2b1c02d4d..0e516ea0f 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -211,7 +211,7 @@ impl LanguageState { /// Clear all cached resources. pub fn clear_cache(&mut self, _arguments: Vec) -> AnySchedulableResponse { comemo::evict(0); - self.primary().clear_cache(); + self.handle.analysis.clear_cache(); just_ok(JsonValue::Null) } @@ -252,8 +252,8 @@ impl LanguageState { ) -> SchedulableResponse { use std::path::Path; + use crate::project::ProjectExt; use crate::tool::preview::PreviewCliArgs; - use actor::typ_client::ProjectExt; use clap::Parser; let cli_args = get_arg_or_default!(args[0] as Vec); @@ -285,7 +285,7 @@ impl LanguageState { let previewer = typst_preview::PreviewBuilder::new(cli_args.preview.clone()); - let handle = &mut self.primary().handle; + let handle = &mut self.handle; let primary = &mut handle.wrapper.primary; let _ = ProjectExt::unregister_preview; if !cli_args.not_as_primary && primary.ext.register_preview(previewer.compile_watcher()) { @@ -358,7 +358,7 @@ impl LanguageState { let from_source = get_arg!(args[0] as String); let to_path = get_arg!(args[1] as Option).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)?; @@ -403,7 +403,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)?; @@ -471,7 +471,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 { @@ -539,13 +539,13 @@ impl LanguageState { impl LanguageState { /// Get the all valid fonts pub fn resource_fonts(&mut self, _arguments: Vec) -> 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) -> 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)) } @@ -562,7 +562,7 @@ impl LanguageState { /// Get directory of packages pub fn resource_package_dirs(&mut self, _arguments: Vec) -> 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(); @@ -576,7 +576,7 @@ impl LanguageState { &mut self, _arguments: Vec, ) -> 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(); @@ -592,7 +592,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 = @@ -610,7 +610,7 @@ impl LanguageState { &mut self, mut arguments: Vec, ) -> 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 { @@ -670,7 +670,7 @@ impl LanguageState { info: PackageInfo, f: impl FnOnce(&mut LocalContextGuard) -> LspResult + Send + Sync, ) -> LspResult>> { - 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)?; diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index 708d49111..d287b48d7 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -21,6 +21,7 @@ mod actor; mod cmd; mod init; +mod project; mod resource; mod server; mod stats; @@ -35,12 +36,13 @@ pub use server::*; pub use sync_lsp::LspClient; pub use tinymist_query as query; pub use tinymist_world as world; -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; diff --git a/crates/tinymist/src/main.rs b/crates/tinymist/src/main.rs index 89b6dab0a..fbe6d7e8b 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist/src/main.rs @@ -217,7 +217,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(); @@ -266,7 +266,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 { diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/project.rs similarity index 60% rename from crates/tinymist/src/actor/typ_client.rs rename to crates/tinymist/src/project.rs index 861d32c01..2e4fa94a2 100644 --- a/crates/tinymist/src/actor/typ_client.rs +++ b/crates/tinymist/src/project.rs @@ -1,12 +1,8 @@ //! The actor that runs compilations. //! //! ```ascii -//! ┌────────────────────────────────┐ -//! │ main::compile_actor (client) │ -//! └─────┬────────────────────▲─────┘ -//! │ │ -//! ┌─────▼────────────────────┴─────┐ ┌────────────┐ -//! │compiler::compile_actor (server)│◄───────►│notify_actor│ +//! ┌────────────────────────────────┐ ┌────────────┐ +//! │ main::compile_actor │◄───────►│notify_actor│ //! └─────┬────────────────────▲─────┘ └────────────┘ //! │ │ //! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐ @@ -14,44 +10,38 @@ //! └────────────────────────────────┘ └────────────┘ //! ``` //! -//! 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`]. +//! 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 [`CompileHandler`] will push information to other actors. +//! The [`LspProjectHandler`] will push information to other actors. -use std::{collections::HashMap, ops::Deref, sync::Arc}; +use std::sync::Arc; use anyhow::bail; use log::{error, info, trace}; use reflexo::path::unix_slash; use reflexo_typst::{ - error::prelude::*, typst::prelude::*, vfs::notify::MemoryEvent, world::EntryState, - CompileReport, Compiler, EntryReader, Error, ImmutPath, + error::prelude::*, typst::prelude::*, vfs::notify::MemoryEvent, CompileReport, EntryReader, }; -use sync_lsp::{just_future, LspClient}; +use sync_lsp::LspClient; use tinymist_query::{ analysis::{Analysis, AnalysisRevLock, LocalContextGuard}, - CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, EntryResolver, OnExportRequest, - SemanticRequest, ServerInfoResponse, StatefulRequest, VersionedDocument, + CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, SemanticRequest, StatefulRequest, + VersionedDocument, }; use tinymist_world::project::{ - update_lock, CompileSnapshot, CompiledArtifact, Interrupt, ProjectCompiler, ProjectHandle, - PROJECT_ROUTE_USER_ACTION_PRIORITY, + CompileSnapshot, CompiledArtifact, Interrupt, ProjectCompiler, ProjectHandle, }; use tinymist_world::{LspInterrupt, TaskInputs}; use tokio::sync::{mpsc, oneshot}; use typst::{diag::SourceDiagnostic, World}; -use super::editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum}; -use crate::{ - stats::{CompilerQueryStats, QueryStatGuard}, - task::{ExportTask, ExportUserConfig}, - world::{LspCompilerFeat, LspWorld}, - CompileConfig, QueryFuture, -}; +use crate::actor::editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum}; +use crate::stats::{CompilerQueryStats, QueryStatGuard}; +use crate::task::ExportTask; +use crate::world::{LspCompilerFeat, LspWorld}; type EditorSender = mpsc::UnboundedSender; @@ -324,207 +314,6 @@ impl ProjectHandle for LspProjectHandler { } } -pub struct CompileClientActor { - pub handle: LocalCompileHandler, - - pub config: CompileConfig, - entry: EntryState, -} - -impl CompileClientActor { - pub(crate) fn new( - handle: LocalCompileHandler, - config: CompileConfig, - entry: EntryState, - ) -> Self { - Self { - handle, - config, - entry, - } - } - - /// Snapshot the compiler thread for tasks - pub fn snapshot(&mut self) -> ZResult { - self.handle.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(&mut self) -> ZResult { - self.handle.query_snapshot(None) - } - - /// Snapshot the compiler thread for language queries - 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.query_snapshot(Some(q))?; - Ok(QuerySnapWithStat { fut, stat }) - } - - pub fn add_memory_changes(&mut self, event: MemoryEvent) { - self.handle.add_memory_changes(event); - } - - 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); - } - - 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(&mut self, req: OnExportRequest) -> QueryFuture { - let OnExportRequest { path, kind, open } = req; - - 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 - // 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::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) -> Result { - 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(&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(); - 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, diff --git a/crates/tinymist/src/resource/fonts.rs b/crates/tinymist/src/resource/fonts.rs index 402beca48..a258322f3 100644 --- a/crates/tinymist/src/resource/fonts.rs +++ b/crates/tinymist/src/resource/fonts.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use typst::text::{FontStretch, FontStyle, FontWeight}; use super::prelude::*; -use crate::actor::typ_client::WorldSnapFut; +use crate::project::WorldSnapFut; #[derive(Debug, Clone, Serialize, Deserialize)] struct FontResourceItem { diff --git a/crates/tinymist/src/resource/symbols.rs b/crates/tinymist/src/resource/symbols.rs index 3ccefb764..06c17f63a 100644 --- a/crates/tinymist/src/resource/symbols.rs +++ b/crates/tinymist/src/resource/symbols.rs @@ -8,7 +8,7 @@ use sync_lsp::LspResult; use tinymist_world::TaskInputs; use typst::{syntax::VirtualPath, World}; -use crate::{actor::typ_client::WorldSnapFut, z_internal_error}; +use crate::{project::WorldSnapFut, z_internal_error}; use super::prelude::*; diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index fc604e956..078916be1 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -1,6 +1,7 @@ //! tinymist's language server use std::collections::HashMap; +use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -12,6 +13,12 @@ use lsp_server::RequestId; use lsp_types::request::{GotoDeclarationParams, WorkspaceConfiguration}; use lsp_types::*; use once_cell::sync::OnceCell; +use project::{ + LocalCompileHandler, LspProjectHandler, QuerySnapFut, QuerySnapWithStat, WorldSnapFut, +}; +use reflexo_typst::watch_deps; +use reflexo_typst::world::EntryState; +use reflexo_typst::Compiler; use reflexo_typst::{ error::prelude::*, vfs::notify::{FileChangeSet, MemoryEvent}, @@ -21,19 +28,31 @@ 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::{ + CacheTask, ExportConfig, ExportTask, ExportUserConfig, FormatTask, FormatterConfig, + UserActionTask, +}; +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_world::project::{update_lock, PROJECT_ROUTE_USER_ACTION_PRIORITY}; +use tinymist_world::project::{CompileServerOpts, ProjectCompiler}; +use tinymist_world::LspInterrupt; use tinymist_world::TaskInputs; use tokio::sync::mpsc; +use typst::layout::Position as TypstPosition; use typst::{diag::FileResult, syntax::Source}; +use crate::stats::CompilerQueryStats; +use crate::world::{ImmutDict, LspUniverseBuilder}; + use super::{init::*, *}; use crate::actor::editor::EditorRequest; -use crate::actor::typ_client::CompileClientActor; pub(crate) use futures::Future; @@ -53,6 +72,8 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) { pub struct LanguageState { /// The lsp client pub client: TypedLspClient, + /// The handle + pub handle: LocalCompileHandler, // State to synchronize with the client. /// Whether the server has registered semantic tokens capabilities. @@ -80,10 +101,6 @@ pub struct LanguageState { pub preview: tool::preview::PreviewState, /// The diagnostics sender to send diagnostics to `crate::actor::cluster`. pub editor_tx: mpsc::UnboundedSender, - /// The primary compiler actor. - pub primary: Option, - /// The compiler actors for tasks - pub dedicates: Vec, /// The formatter tasks running in backend, which will be scheduled by async /// runtime. pub formatter: FormatTask, @@ -104,11 +121,20 @@ impl LanguageState { ) -> Self { let formatter = FormatTask::new(config.formatter()); + let default_path = config.compile.entry_resolver.resolve_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(), + ); + Self { client: client.clone(), + 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)), @@ -161,16 +187,6 @@ 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(&mut self) -> &mut CompileClientActor { - self.primary.as_mut().expect("primary") - } - /// Install handlers to the language server. pub fn install + AddCommands + 'static>( provider: LspBuilder, @@ -275,8 +291,7 @@ impl LanguageState { return Ok(()); }; - let server = ready.primary(); - server.interrupt(params); + ready.handle.interrupt(params); Ok(()) } } @@ -520,10 +535,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 { @@ -532,10 +543,7 @@ 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() { @@ -830,11 +838,24 @@ impl LanguageState { impl LanguageState { /// Focus main file to some path. - pub fn do_change_entry(&mut self, new_entry: Option) -> Result { - self.primary - .as_mut() - .unwrap() - .change_entry(new_entry.clone()) + pub fn change_entry(&mut self, path: Option) -> Result { + 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 @@ -843,7 +864,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 @@ -853,7 +874,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 @@ -899,12 +920,330 @@ impl LanguageState { Ok(false) => {} } } + + /// Snapshot the compiler thread for tasks + pub fn snapshot(&mut self) -> ZResult { + self.handle.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 { + self.handle.query_snapshot(None) + } + + /// Snapshot the compiler thread for language queries + 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.query_snapshot(Some(q))?; + Ok(QuerySnapWithStat { fut, stat }) + } + + fn add_memory_changes(&mut self, event: MemoryEvent) { + self.handle.add_memory_changes(event); + } + + fn change_task(&mut self, task_inputs: TaskInputs) { + self.handle.change_task(task_inputs); + } + + pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) { + self.handle.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.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)) + }) + } + + /// Export the current document. + pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture { + let OnExportRequest { path, kind, open } = req; + + 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 + // 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::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) { + 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) { + self.restart_server(dedicate, entry); + } + + /// Restart the server with the given group. + fn restart_server(&mut self, group: &str, entry: Option) { + let _ = group; + let _ = entry; + // let server = self.server( + // group.to_owned(), + // self.entry_resolver().resolve(entry), + // self.compile_config().determine_inputs(), + // ); + + // 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 + // } + // todo!() + // }; + + // if let Some(mut prev) = prev { + // self.client.handle.spawn(async move { prev.settle().await }); + // } + + todo!() + } + + /// Create a new server for the given group. + pub fn server( + config: &Config, + editor_tx: tokio::sync::mpsc::UnboundedSender, + client: TypedLspClient, + diag_group: String, + entry: EntryState, + inputs: ImmutDict, + ) -> LocalCompileHandler { + 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(LspProjectHandler { + #[cfg(feature = "preview")] + inner: std::sync::Arc::new(parking_lot::RwLock::new(None)), + 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 + }), + 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_with( + verse, + dep_tx, + CompileServerOpts { + compile_handle, + ..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)); + LocalCompileHandler { + diag_group, + wrapper: server, + 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 { + self.0.render_marked(ctx, doc, pos) + } } impl LanguageState { fn update_source(&mut self, files: FileChangeSet) -> Result<(), Error> { - self.primary() - .add_memory_changes(MemoryEvent::Update(files.clone())); + self.add_memory_changes(MemoryEvent::Update(files.clone())); Ok(()) } @@ -1014,27 +1353,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 self.primary().on_export(req), - ServerInfo(_) => return self.primary().collect_server_info(), - _ => return Self::query_on(self.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: &mut 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)) }); diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index 641dee874..d66afb889 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -23,9 +23,7 @@ use typst_pdf::PdfOptions; use crate::tool::text::FullTextDigest; use crate::{ - actor::{editor::EditorRequest, typ_client::WorldSnapFut}, - tool::word_count, - world::LspCompilerFeat, + actor::editor::EditorRequest, project::WorldSnapFut, tool::word_count, world::LspCompilerFeat, ExportMode, PathPattern, }; diff --git a/crates/tinymist/src/task/user_action.rs b/crates/tinymist/src/task/user_action.rs index f2fc6936a..36f7f7664 100644 --- a/crates/tinymist/src/task/user_action.rs +++ b/crates/tinymist/src/task/user_action.rs @@ -178,7 +178,7 @@ async fn trace_main( let timings = writer.into_inner().unwrap(); - let handle = &state.primary().handle; + let handle = &state.handle; let diagnostics = tinymist_query::convert_diagnostics(w, diags.iter(), handle.analysis.position_encoding); diff --git a/crates/tinymist/src/tool/preview.rs b/crates/tinymist/src/tool/preview.rs index df5b95ef3..a3122c5b8 100644 --- a/crates/tinymist/src/tool/preview.rs +++ b/crates/tinymist/src/tool/preview.rs @@ -11,7 +11,6 @@ use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStre use hyper_util::rt::TokioIo; use hyper_util::server::graceful::GracefulShutdown; use lsp_types::notification::Notification; -use project::ProjectCompiler; use reflexo_typst::debug_loc::SourceSpanOffset; use reflexo_typst::vfs::notify::{FileChangeSet, MemoryEvent}; use reflexo_typst::watch_deps; @@ -34,10 +33,10 @@ use typst_preview::{ }; use typst_shim::syntax::LinkedNodeExt; -use crate::world::LspCompilerFeat; +use crate::project::{LspProjectHandler, ProjectClient}; +use crate::world::{project::ProjectCompiler, LspCompilerFeat}; use crate::*; use actor::preview::{PreviewActor, PreviewRequest, PreviewTab}; -use actor::typ_client::{LspProjectHandler, ProjectClient}; /// The preview's view of the compiled artifact. pub struct PreviewCompileView {