feat: support pdf export

This commit is contained in:
Myriad-Dreamin 2024-03-10 23:14:17 +08:00
parent 7079360096
commit e3f4588c96
6 changed files with 173 additions and 50 deletions

View file

@ -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 {

View file

@ -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<ImmutPath>),
ChangeConfig(PdfExportConfig),
}
#[derive(Debug, Clone)]
pub struct PdfExportConfig {
path: PathBuf,
mode: ExportPdfMode,
pub path: Option<ImmutPath>,
pub mode: ExportPdfMode,
}
pub struct PdfExportActor {
render_rx: broadcast::Receiver<RenderActorRequest>,
document: watch::Receiver<Option<Arc<TypstDocument>>>,
config: Option<PdfExportConfig>,
pub path: Option<ImmutPath>,
pub mode: ExportPdfMode,
}
impl PdfExportActor {
pub fn new(
document: watch::Receiver<Option<Arc<TypstDocument>>>,
render_rx: broadcast::Receiver<RenderActorRequest>,
config: Option<PdfExportConfig>,
) -> 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(())
}
}

View file

@ -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<SyncMutex<Option<ImmutPath>>>,
entry: Arc<Mutex<Option<ImmutPath>>>,
pub inner: CompileClient<CompileHandler>,
render_tx: broadcast::Sender<RenderActorRequest>,
}
// 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<CompileHandler>,
render_tx: broadcast::Sender<RenderActorRequest>,
) -> 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(())
}

View file

@ -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(&params);
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,

View file

@ -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<String, JsonValue>) -> 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<JsonValue> = serde_json::from_value(result).unwrap();
let _ = this.on_changed_configuration(Config::values_to_map(resp));
},
);
}

View file

@ -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.",