Skip to content

Commit e3f4588

Browse files
feat: support pdf export
1 parent 7079360 commit e3f4588

File tree

6 files changed

+173
-50
lines changed

6 files changed

+173
-50
lines changed

crates/tinymist/src/actor.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tokio::sync::{broadcast, watch};
1212
use typst_ts_core::config::CompileOpts;
1313

1414
use self::{
15-
render::PdfExportActor,
15+
render::{PdfExportActor, PdfExportConfig},
1616
typst::{create_server, CompileActor},
1717
};
1818
use crate::TypstLanguageServer;
@@ -23,7 +23,19 @@ impl TypstLanguageServer {
2323
let (render_tx, _) = broadcast::channel(10);
2424

2525
// Run the PDF export actor before preparing cluster to avoid loss of events
26-
tokio::spawn(PdfExportActor::new(doc_rx.clone(), render_tx.subscribe()).run());
26+
tokio::spawn(
27+
PdfExportActor::new(
28+
doc_rx.clone(),
29+
render_tx.subscribe(),
30+
Some(PdfExportConfig {
31+
path: entry
32+
.as_ref()
33+
.map(|e| e.clone().with_extension("pdf").into()),
34+
mode: self.config.export_pdf,
35+
}),
36+
)
37+
.run(),
38+
);
2739

2840
let roots = self.roots.clone();
2941
let opts = CompileOpts {

crates/tinymist/src/actor/render.rs

+70-24
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,49 @@ use std::{
66
};
77

88
use anyhow::Context;
9-
use log::info;
9+
use log::{error, info};
1010
use tokio::sync::{
1111
broadcast::{self, error::RecvError},
1212
watch,
1313
};
1414
use typst::foundations::Smart;
15-
use typst_ts_core::TypstDocument;
15+
use typst_ts_core::{ImmutPath, TypstDocument};
1616

1717
use crate::ExportPdfMode;
1818

1919
#[derive(Debug, Clone)]
2020
pub enum RenderActorRequest {
21-
Render,
22-
// ChangeConfig(PdfExportConfig),
21+
OnTyped,
22+
OnSaved(PathBuf),
23+
ChangeExportPath(Option<ImmutPath>),
24+
ChangeConfig(PdfExportConfig),
2325
}
2426

2527
#[derive(Debug, Clone)]
2628
pub struct PdfExportConfig {
27-
path: PathBuf,
28-
mode: ExportPdfMode,
29+
pub path: Option<ImmutPath>,
30+
pub mode: ExportPdfMode,
2931
}
3032

3133
pub struct PdfExportActor {
3234
render_rx: broadcast::Receiver<RenderActorRequest>,
3335
document: watch::Receiver<Option<Arc<TypstDocument>>>,
3436

35-
config: Option<PdfExportConfig>,
37+
pub path: Option<ImmutPath>,
38+
pub mode: ExportPdfMode,
3639
}
3740

3841
impl PdfExportActor {
3942
pub fn new(
4043
document: watch::Receiver<Option<Arc<TypstDocument>>>,
4144
render_rx: broadcast::Receiver<RenderActorRequest>,
45+
config: Option<PdfExportConfig>,
4246
) -> Self {
4347
Self {
4448
render_rx,
4549
document,
46-
47-
config: None,
50+
path: config.as_ref().and_then(|c| c.path.clone()),
51+
mode: config.map(|c| c.mode).unwrap_or(ExportPdfMode::Auto),
4852
}
4953
}
5054

@@ -65,28 +69,71 @@ impl PdfExportActor {
6569

6670
};
6771

72+
info!("PdfRenderActor: received request: {req:?}", req = req);
6873
match req {
69-
RenderActorRequest::Render => {
70-
let Some(document) = self.document.borrow().clone() else {
71-
info!("PdfRenderActor: document is not ready");
72-
continue;
73-
};
74-
75-
if let Some(cfg) = self.config.as_ref() {
76-
if cfg.mode == ExportPdfMode::OnType {
77-
self.export_pdf(&document, &cfg.path).await.unwrap();
78-
}
79-
}
74+
RenderActorRequest::ChangeConfig(cfg) => {
75+
self.path = cfg.path;
76+
self.mode = cfg.mode;
77+
}
78+
RenderActorRequest::ChangeExportPath(cfg) => {
79+
self.path = cfg;
80+
}
81+
_ => {
82+
self.check_mode_and_export(req).await;
8083
}
81-
// RenderActorRequest::ChangeConfig(config) => {
82-
// self.config = Some(config);
83-
// }
8484
}
8585
}
8686
}
8787
}
8888
}
8989

90+
async fn check_mode_and_export(&self, req: RenderActorRequest) {
91+
let Some(document) = self.document.borrow().clone() else {
92+
info!("PdfRenderActor: document is not ready");
93+
return;
94+
};
95+
96+
let eq_mode = match req {
97+
RenderActorRequest::OnTyped => ExportPdfMode::OnType,
98+
RenderActorRequest::OnSaved(..) => ExportPdfMode::OnSave,
99+
_ => unreachable!(),
100+
};
101+
102+
info!("PdfRenderActor: check path {:?}", self.path);
103+
if let Some(path) = self.path.as_ref() {
104+
if (get_mode(self.mode) == eq_mode) || validate_document(&req, self.mode, &document) {
105+
let Err(err) = self.export_pdf(&document, path).await else {
106+
return;
107+
};
108+
error!("PdfRenderActor: failed to export PDF: {err}", err = err);
109+
}
110+
}
111+
112+
fn get_mode(mode: ExportPdfMode) -> ExportPdfMode {
113+
if mode == ExportPdfMode::Auto {
114+
return ExportPdfMode::Never;
115+
}
116+
117+
mode
118+
}
119+
120+
fn validate_document(
121+
req: &RenderActorRequest,
122+
mode: ExportPdfMode,
123+
document: &TypstDocument,
124+
) -> bool {
125+
info!(
126+
"PdfRenderActor: validating document for export mode {mode:?} title is {title}",
127+
title = document.title.is_some()
128+
);
129+
if mode == ExportPdfMode::OnDocumentHasTitle {
130+
return document.title.is_some() && matches!(req, RenderActorRequest::OnSaved(..));
131+
}
132+
133+
false
134+
}
135+
}
136+
90137
async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<()> {
91138
// todo: Some(pdf_uri.as_str())
92139
// todo: timestamp world.now()
@@ -97,7 +144,6 @@ impl PdfExportActor {
97144
std::fs::write(path, data).context("failed to export PDF")?;
98145

99146
info!("PDF export complete");
100-
101147
Ok(())
102148
}
103149
}

crates/tinymist/src/actor/typst.rs

+51-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::{
55
sync::{Arc, Mutex as SyncMutex},
66
};
77

8-
use log::{debug, error, trace, warn};
8+
use log::{debug, error, info, trace, warn};
9+
use parking_lot::Mutex;
910
use tinymist_query::{
1011
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature,
1112
OnSaveExportRequest, PositionEncoding,
@@ -34,8 +35,8 @@ use typst_ts_core::{
3435
Bytes, Error, ImmutPath, TypstDocument, TypstWorld,
3536
};
3637

37-
use super::compile::CompileActor as CompileActorInner;
3838
use super::compile::CompileClient as TsCompileClient;
39+
use super::{compile::CompileActor as CompileActorInner, render::PdfExportConfig};
3940
use crate::actor::render::RenderActorRequest;
4041
use crate::ConstConfig;
4142

@@ -62,11 +63,12 @@ pub fn create_server(
6263
let root = compiler_driver.inner.world.root.as_ref().to_owned();
6364
let handler: CompileHandler = compiler_driver.handler.clone();
6465

66+
let ontyped_render_tx = render_tx.clone();
6567
let driver = CompileExporter::new(compiler_driver).with_exporter(Box::new(
6668
move |_w: &dyn TypstWorld, doc| {
6769
let _ = doc_sender.send(Some(doc));
6870
// todo: is it right that ignore zero broadcast receiver?
69-
let _ = render_tx.send(RenderActorRequest::Render);
71+
let _ = ontyped_render_tx.send(RenderActorRequest::OnTyped);
7072

7173
Ok(())
7274
},
@@ -84,11 +86,17 @@ pub fn create_server(
8486

8587
current_runtime.spawn(server.spawn());
8688

87-
let this = CompileActor::new(diag_group, cfg.position_encoding, handler, client);
89+
let this = CompileActor::new(
90+
diag_group,
91+
cfg.position_encoding,
92+
handler,
93+
client,
94+
render_tx,
95+
);
8896

8997
// todo: less bug-prone code
9098
if let Some(entry) = entry {
91-
this.entry.lock().unwrap().replace(entry.into());
99+
this.entry.lock().replace(entry.into());
92100
}
93101

94102
this
@@ -289,8 +297,9 @@ pub struct CompileActor {
289297
diag_group: String,
290298
position_encoding: PositionEncoding,
291299
handler: CompileHandler,
292-
entry: Arc<SyncMutex<Option<ImmutPath>>>,
300+
entry: Arc<Mutex<Option<ImmutPath>>>,
293301
pub inner: CompileClient<CompileHandler>,
302+
render_tx: broadcast::Sender<RenderActorRequest>,
294303
}
295304

296305
// todo: remove unsafe impl send
@@ -356,7 +365,7 @@ impl CompileActor {
356365
// todo: more robust rollback logic
357366
let entry = self.entry.clone();
358367
let should_change = {
359-
let mut entry = entry.lock().unwrap();
368+
let mut entry = entry.lock();
360369
let should_change = entry.as_ref().map(|e| e != &path).unwrap_or(true);
361370
let prev = entry.clone();
362371
*entry = Some(path.clone());
@@ -373,6 +382,12 @@ impl CompileActor {
373382
next.display()
374383
);
375384

385+
self.render_tx
386+
.send(RenderActorRequest::ChangeExportPath(Some(
387+
next.with_extension("pdf").into(),
388+
)))
389+
.unwrap();
390+
376391
// todo
377392
let res = self.steal(move |compiler| {
378393
let root = compiler.compiler.world().workspace_root();
@@ -386,7 +401,13 @@ impl CompileActor {
386401
});
387402

388403
if res.is_err() {
389-
let mut entry = entry.lock().unwrap();
404+
self.render_tx
405+
.send(RenderActorRequest::ChangeExportPath(
406+
prev.clone().map(|e| e.with_extension("pdf").into()),
407+
))
408+
.unwrap();
409+
410+
let mut entry = entry.lock();
390411
if *entry == Some(next) {
391412
*entry = prev;
392413
}
@@ -396,11 +417,25 @@ impl CompileActor {
396417

397418
// todo: trigger recompile
398419
let files = FileChangeSet::new_inserts(vec![]);
399-
self.inner.add_memory_changes(MemoryEvent::Update(files))
420+
self.inner.add_memory_changes(MemoryEvent::Update(files));
400421
}
401422

402423
Ok(())
403424
}
425+
426+
pub(crate) fn change_export_pdf(&self, export_pdf: crate::ExportPdfMode) {
427+
let entry = self.entry.lock();
428+
let path = entry
429+
.as_ref()
430+
.map(|e| e.clone().with_extension("pdf").into());
431+
let _ = self
432+
.render_tx
433+
.send(RenderActorRequest::ChangeConfig(PdfExportConfig {
434+
path,
435+
mode: export_pdf,
436+
}))
437+
.unwrap();
438+
}
404439
}
405440

406441
impl SourceFileServer for CompileActor {
@@ -494,13 +529,15 @@ impl CompileActor {
494529
position_encoding: PositionEncoding,
495530
handler: CompileHandler,
496531
inner: CompileClient<CompileHandler>,
532+
render_tx: broadcast::Sender<RenderActorRequest>,
497533
) -> Self {
498534
Self {
499535
diag_group,
500536
position_encoding,
501537
handler,
502-
entry: Arc::new(SyncMutex::new(None)),
538+
entry: Arc::new(Mutex::new(None)),
503539
inner,
540+
render_tx,
504541
}
505542
}
506543

@@ -529,7 +566,10 @@ impl CompileActor {
529566
}
530567
}
531568

532-
fn on_save_export(&self, _path: PathBuf) -> anyhow::Result<()> {
569+
fn on_save_export(&self, path: PathBuf) -> anyhow::Result<()> {
570+
info!("CompileActor: on save export: {}", path.display());
571+
let _ = self.render_tx.send(RenderActorRequest::OnSaved(path));
572+
533573
Ok(())
534574
}
535575

crates/tinymist/src/init.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
33

44
use anyhow::bail;
55
use itertools::Itertools;
6+
use log::info;
67
use lsp_types::*;
78
use serde::Deserialize;
89
use serde_json::{Map, Value as JsonValue};
@@ -114,14 +115,18 @@ pub enum ExperimentalFormatterMode {
114115
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
115116
#[serde(rename_all = "camelCase")]
116117
pub enum ExportPdfMode {
117-
/// Don't export PDF automatically.
118+
#[default]
119+
Auto,
120+
/// Select best solution automatically. (Recommended)
118121
Never,
119122
/// Export PDF on saving the document, i.e. on `textDocument/didSave`
120123
/// events.
121-
#[default]
122124
OnSave,
123125
/// Export PDF on typing, i.e. on `textDocument/didChange` events.
124126
OnType,
127+
/// Export PDFs when a document has a title, which is useful to filter out
128+
/// template files.
129+
OnDocumentHasTitle,
125130
}
126131

127132
/// The mode of semantic tokens.
@@ -346,6 +351,11 @@ impl Init {
346351

347352
// Initialize configurations
348353
let cc = ConstConfig::from(&params);
354+
info!(
355+
"initialized with const_config {const_config:?}",
356+
const_config = cc
357+
);
358+
349359
let mut config = Config::default();
350360

351361
// Bootstrap server
@@ -364,6 +374,8 @@ impl Init {
364374
}
365375
}
366376

377+
info!("initialized with config {config:?}", config = config);
378+
367379
let cluster_actor = CompileClusterActor {
368380
host: self.host.clone(),
369381
diag_rx,

0 commit comments

Comments
 (0)