From e3f4588c96388ee4efe14b4fed8f8490c396165a Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Sun, 10 Mar 2024 23:14:17 +0800 Subject: [PATCH] feat: support pdf export --- crates/tinymist/src/actor.rs | 16 ++++- crates/tinymist/src/actor/render.rs | 94 +++++++++++++++++++++-------- crates/tinymist/src/actor/typst.rs | 62 +++++++++++++++---- crates/tinymist/src/init.rs | 16 ++++- crates/tinymist/src/lib.rs | 19 ++++-- editors/vscode/package.json | 16 +++-- 6 files changed, 173 insertions(+), 50 deletions(-) diff --git a/crates/tinymist/src/actor.rs b/crates/tinymist/src/actor.rs index badd68dc..99920bba 100644 --- a/crates/tinymist/src/actor.rs +++ b/crates/tinymist/src/actor.rs @@ -12,7 +12,7 @@ use tokio::sync::{broadcast, watch}; use typst_ts_core::config::CompileOpts; use self::{ - render::PdfExportActor, + render::{PdfExportActor, PdfExportConfig}, typst::{create_server, CompileActor}, }; use crate::TypstLanguageServer; @@ -23,7 +23,19 @@ impl TypstLanguageServer { let (render_tx, _) = broadcast::channel(10); // Run the PDF export actor before preparing cluster to avoid loss of events - tokio::spawn(PdfExportActor::new(doc_rx.clone(), render_tx.subscribe()).run()); + tokio::spawn( + PdfExportActor::new( + doc_rx.clone(), + render_tx.subscribe(), + Some(PdfExportConfig { + path: entry + .as_ref() + .map(|e| e.clone().with_extension("pdf").into()), + mode: self.config.export_pdf, + }), + ) + .run(), + ); let roots = self.roots.clone(); let opts = CompileOpts { diff --git a/crates/tinymist/src/actor/render.rs b/crates/tinymist/src/actor/render.rs index e02ae706..b50d6535 100644 --- a/crates/tinymist/src/actor/render.rs +++ b/crates/tinymist/src/actor/render.rs @@ -6,45 +6,49 @@ use std::{ }; use anyhow::Context; -use log::info; +use log::{error, info}; use tokio::sync::{ broadcast::{self, error::RecvError}, watch, }; use typst::foundations::Smart; -use typst_ts_core::TypstDocument; +use typst_ts_core::{ImmutPath, TypstDocument}; use crate::ExportPdfMode; #[derive(Debug, Clone)] pub enum RenderActorRequest { - Render, - // ChangeConfig(PdfExportConfig), + OnTyped, + OnSaved(PathBuf), + ChangeExportPath(Option), + ChangeConfig(PdfExportConfig), } #[derive(Debug, Clone)] pub struct PdfExportConfig { - path: PathBuf, - mode: ExportPdfMode, + pub path: Option, + pub mode: ExportPdfMode, } pub struct PdfExportActor { render_rx: broadcast::Receiver, document: watch::Receiver>>, - config: Option, + pub path: Option, + pub mode: ExportPdfMode, } impl PdfExportActor { pub fn new( document: watch::Receiver>>, render_rx: broadcast::Receiver, + config: Option, ) -> Self { Self { render_rx, document, - - config: None, + path: config.as_ref().and_then(|c| c.path.clone()), + mode: config.map(|c| c.mode).unwrap_or(ExportPdfMode::Auto), } } @@ -65,28 +69,71 @@ impl PdfExportActor { }; + info!("PdfRenderActor: received request: {req:?}", req = req); match req { - RenderActorRequest::Render => { - let Some(document) = self.document.borrow().clone() else { - info!("PdfRenderActor: document is not ready"); - continue; - }; - - if let Some(cfg) = self.config.as_ref() { - if cfg.mode == ExportPdfMode::OnType { - self.export_pdf(&document, &cfg.path).await.unwrap(); - } - } + RenderActorRequest::ChangeConfig(cfg) => { + self.path = cfg.path; + self.mode = cfg.mode; + } + RenderActorRequest::ChangeExportPath(cfg) => { + self.path = cfg; + } + _ => { + self.check_mode_and_export(req).await; } - // RenderActorRequest::ChangeConfig(config) => { - // self.config = Some(config); - // } } } } } } + async fn check_mode_and_export(&self, req: RenderActorRequest) { + let Some(document) = self.document.borrow().clone() else { + info!("PdfRenderActor: document is not ready"); + return; + }; + + let eq_mode = match req { + RenderActorRequest::OnTyped => ExportPdfMode::OnType, + RenderActorRequest::OnSaved(..) => ExportPdfMode::OnSave, + _ => unreachable!(), + }; + + info!("PdfRenderActor: check path {:?}", self.path); + if let Some(path) = self.path.as_ref() { + if (get_mode(self.mode) == eq_mode) || validate_document(&req, self.mode, &document) { + let Err(err) = self.export_pdf(&document, path).await else { + return; + }; + error!("PdfRenderActor: failed to export PDF: {err}", err = err); + } + } + + fn get_mode(mode: ExportPdfMode) -> ExportPdfMode { + if mode == ExportPdfMode::Auto { + return ExportPdfMode::Never; + } + + mode + } + + fn validate_document( + req: &RenderActorRequest, + mode: ExportPdfMode, + document: &TypstDocument, + ) -> bool { + info!( + "PdfRenderActor: validating document for export mode {mode:?} title is {title}", + title = document.title.is_some() + ); + if mode == ExportPdfMode::OnDocumentHasTitle { + return document.title.is_some() && matches!(req, RenderActorRequest::OnSaved(..)); + } + + false + } + } + async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<()> { // todo: Some(pdf_uri.as_str()) // todo: timestamp world.now() @@ -97,7 +144,6 @@ impl PdfExportActor { std::fs::write(path, data).context("failed to export PDF")?; info!("PDF export complete"); - Ok(()) } } diff --git a/crates/tinymist/src/actor/typst.rs b/crates/tinymist/src/actor/typst.rs index 97150187..517245a9 100644 --- a/crates/tinymist/src/actor/typst.rs +++ b/crates/tinymist/src/actor/typst.rs @@ -5,7 +5,8 @@ use std::{ sync::{Arc, Mutex as SyncMutex}, }; -use log::{debug, error, trace, warn}; +use log::{debug, error, info, trace, warn}; +use parking_lot::Mutex; use tinymist_query::{ CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature, OnSaveExportRequest, PositionEncoding, @@ -34,8 +35,8 @@ use typst_ts_core::{ Bytes, Error, ImmutPath, TypstDocument, TypstWorld, }; -use super::compile::CompileActor as CompileActorInner; use super::compile::CompileClient as TsCompileClient; +use super::{compile::CompileActor as CompileActorInner, render::PdfExportConfig}; use crate::actor::render::RenderActorRequest; use crate::ConstConfig; @@ -62,11 +63,12 @@ pub fn create_server( let root = compiler_driver.inner.world.root.as_ref().to_owned(); let handler: CompileHandler = compiler_driver.handler.clone(); + let ontyped_render_tx = render_tx.clone(); let driver = CompileExporter::new(compiler_driver).with_exporter(Box::new( move |_w: &dyn TypstWorld, doc| { let _ = doc_sender.send(Some(doc)); // todo: is it right that ignore zero broadcast receiver? - let _ = render_tx.send(RenderActorRequest::Render); + let _ = ontyped_render_tx.send(RenderActorRequest::OnTyped); Ok(()) }, @@ -84,11 +86,17 @@ pub fn create_server( current_runtime.spawn(server.spawn()); - let this = CompileActor::new(diag_group, cfg.position_encoding, handler, client); + let this = CompileActor::new( + diag_group, + cfg.position_encoding, + handler, + client, + render_tx, + ); // todo: less bug-prone code if let Some(entry) = entry { - this.entry.lock().unwrap().replace(entry.into()); + this.entry.lock().replace(entry.into()); } this @@ -289,8 +297,9 @@ pub struct CompileActor { diag_group: String, position_encoding: PositionEncoding, handler: CompileHandler, - entry: Arc>>, + entry: Arc>>, pub inner: CompileClient, + render_tx: broadcast::Sender, } // todo: remove unsafe impl send @@ -356,7 +365,7 @@ impl CompileActor { // todo: more robust rollback logic let entry = self.entry.clone(); let should_change = { - let mut entry = entry.lock().unwrap(); + let mut entry = entry.lock(); let should_change = entry.as_ref().map(|e| e != &path).unwrap_or(true); let prev = entry.clone(); *entry = Some(path.clone()); @@ -373,6 +382,12 @@ impl CompileActor { next.display() ); + self.render_tx + .send(RenderActorRequest::ChangeExportPath(Some( + next.with_extension("pdf").into(), + ))) + .unwrap(); + // todo let res = self.steal(move |compiler| { let root = compiler.compiler.world().workspace_root(); @@ -386,7 +401,13 @@ impl CompileActor { }); if res.is_err() { - let mut entry = entry.lock().unwrap(); + self.render_tx + .send(RenderActorRequest::ChangeExportPath( + prev.clone().map(|e| e.with_extension("pdf").into()), + )) + .unwrap(); + + let mut entry = entry.lock(); if *entry == Some(next) { *entry = prev; } @@ -396,11 +417,25 @@ impl CompileActor { // todo: trigger recompile let files = FileChangeSet::new_inserts(vec![]); - self.inner.add_memory_changes(MemoryEvent::Update(files)) + self.inner.add_memory_changes(MemoryEvent::Update(files)); } Ok(()) } + + pub(crate) fn change_export_pdf(&self, export_pdf: crate::ExportPdfMode) { + let entry = self.entry.lock(); + let path = entry + .as_ref() + .map(|e| e.clone().with_extension("pdf").into()); + let _ = self + .render_tx + .send(RenderActorRequest::ChangeConfig(PdfExportConfig { + path, + mode: export_pdf, + })) + .unwrap(); + } } impl SourceFileServer for CompileActor { @@ -494,13 +529,15 @@ impl CompileActor { position_encoding: PositionEncoding, handler: CompileHandler, inner: CompileClient, + render_tx: broadcast::Sender, ) -> Self { Self { diag_group, position_encoding, handler, - entry: Arc::new(SyncMutex::new(None)), + entry: Arc::new(Mutex::new(None)), inner, + render_tx, } } @@ -529,7 +566,10 @@ impl CompileActor { } } - fn on_save_export(&self, _path: PathBuf) -> anyhow::Result<()> { + fn on_save_export(&self, path: PathBuf) -> anyhow::Result<()> { + info!("CompileActor: on save export: {}", path.display()); + let _ = self.render_tx.send(RenderActorRequest::OnSaved(path)); + Ok(()) } diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index b5879568..6d61072f 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use anyhow::bail; use itertools::Itertools; +use log::info; use lsp_types::*; use serde::Deserialize; use serde_json::{Map, Value as JsonValue}; @@ -114,14 +115,18 @@ pub enum ExperimentalFormatterMode { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub enum ExportPdfMode { - /// Don't export PDF automatically. + #[default] + Auto, + /// Select best solution automatically. (Recommended) Never, /// Export PDF on saving the document, i.e. on `textDocument/didSave` /// events. - #[default] OnSave, /// Export PDF on typing, i.e. on `textDocument/didChange` events. OnType, + /// Export PDFs when a document has a title, which is useful to filter out + /// template files. + OnDocumentHasTitle, } /// The mode of semantic tokens. @@ -346,6 +351,11 @@ impl Init { // Initialize configurations let cc = ConstConfig::from(¶ms); + info!( + "initialized with const_config {const_config:?}", + const_config = cc + ); + let mut config = Config::default(); // Bootstrap server @@ -364,6 +374,8 @@ impl Init { } } + info!("initialized with config {config:?}", config = config); + let cluster_actor = CompileClusterActor { host: self.host.clone(), diag_rx, diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index 67365cd5..171c82cb 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -788,16 +788,25 @@ impl TypstLanguageServer { let uri = params.text_document.uri; let path = uri.to_file_path().unwrap(); - if self.config.export_pdf == ExportPdfMode::OnSave { - let _ = run_query!(self.OnSaveExport(path)); - } + let _ = run_query!(self.OnSaveExport(path)); Ok(()) } fn on_changed_configuration(&mut self, values: Map) -> LspResult<()> { + let export_pdf = self.config.export_pdf; match self.config.update_by_map(&values) { Ok(()) => { info!("new settings applied"); + + if export_pdf != self.config.export_pdf { + self.primary().change_export_pdf(self.config.export_pdf); + { + let m = self.main.lock(); + if let Some(main) = m.as_ref() { + main.wait().change_export_pdf(self.config.export_pdf); + } + } + } } Err(err) => { error!("error applying new settings: {err}"); @@ -829,8 +838,8 @@ impl TypstLanguageServer { return; }; - let resp = serde_json::from_value(result).unwrap(); - let _ = this.on_changed_configuration(resp); + let resp: Vec = serde_json::from_value(result).unwrap(); + let _ = this.on_changed_configuration(Config::values_to_map(resp)); }, ); } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index dfe00157..c709b6e1 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -27,16 +27,20 @@ "title": "Export PDF", "description": "The extension can export PDFs of your Typst files. This setting controls whether this feature is enabled and how often it runs.", "type": "string", - "default": "onSave", + "default": "auto", "enum": [ + "auto", "never", "onSave", - "onType" + "onType", + "onDocumentHasTitle" ], "enumDescriptions": [ + "Select best solution automatically. (Recommended)", "Never export PDFs, you will manually run typst.", "Export PDFs when you save a file.", - "Export PDFs as you type in a file." + "Export PDFs as you type in a file.", + "Export PDFs when a document has a title (and save a file), which is useful to filter out template files." ] }, "tinymist.rootPath": { @@ -86,10 +90,10 @@ "title": "Enable Experimental Formatter", "description": "The extension can format Typst files using typstfmt (experimental).", "type": "string", - "default": "off", + "default": "disable", "enum": [ - "off", - "on" + "disable", + "enable" ], "enumDescriptions": [ "Formatter is not activated.",