mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 21:37:32 +00:00
feat: support pdf export
This commit is contained in:
parent
7079360096
commit
e3f4588c96
6 changed files with 173 additions and 50 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue