diff --git a/Cargo.toml b/Cargo.toml index c9ca5b91..276061d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ codespan-reporting = "0.11" typst = "0.11.0" typst-ide = "0.11.0" typst-pdf = "0.11.0" +typst-svg = "0.11.0" +typst-render = "0.11.0" typst-assets = "0.11.0" reflexo = { version = "0.5.0-rc2", default-features = false, features = [ "flat-vector", @@ -107,6 +109,8 @@ undocumented_unsafe_blocks = "warn" typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } typst-ide = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } +typst-svg = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } +typst-render = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } typst-pdf = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } # typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index 85243d58..1fed457a 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -109,12 +109,40 @@ pub trait StatefulRequest { #[allow(missing_docs)] mod polymorphic { + use serde::{Deserialize, Serialize}; + use super::prelude::*; use super::*; + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] + pub enum PageSelection { + #[serde(rename = "first")] + First, + #[serde(rename = "merged")] + Merged, + } + + #[derive(Debug, Clone)] + pub enum ExportKind { + Pdf, + Svg { page: PageSelection }, + Png { page: PageSelection }, + } + + impl ExportKind { + pub fn extension(&self) -> &str { + match self { + Self::Pdf => "pdf", + Self::Svg { .. } => "svg", + Self::Png { .. } => "png", + } + } + } + #[derive(Debug, Clone)] pub struct OnExportRequest { pub path: PathBuf, + pub kind: ExportKind, } #[derive(Debug, Clone)] diff --git a/crates/tinymist/Cargo.toml b/crates/tinymist/Cargo.toml index 549b6615..25032fb1 100644 --- a/crates/tinymist/Cargo.toml +++ b/crates/tinymist/Cargo.toml @@ -36,7 +36,9 @@ clap_complete_fig.workspace = true clap_mangen.workspace = true typst.workspace = true +typst-svg.workspace = true typst-pdf.workspace = true +typst-render.workspace = true typst-assets = { workspace = true, features = ["fonts"] } typst-ts-core = { workspace = true, default-features = false, features = [ diff --git a/crates/tinymist/src/actor.rs b/crates/tinymist/src/actor.rs index e34b83a7..ff4332ff 100644 --- a/crates/tinymist/src/actor.rs +++ b/crates/tinymist/src/actor.rs @@ -14,7 +14,7 @@ use typst_ts_compiler::{ use typst_ts_core::config::compiler::EntryState; use self::{ - render::{PdfExportActor, PdfExportConfig}, + render::{ExportActor, ExportConfig}, typ_client::{CompileClientActor, CompileDriver, CompileHandler}, typ_server::CompileServerActor, }; @@ -30,12 +30,12 @@ impl TypstLanguageServer { let (doc_tx, doc_rx) = watch::channel(None); let (render_tx, _) = broadcast::channel(10); - // Run the PDF export actor before preparing cluster to avoid loss of events + // Run the Export actor before preparing cluster to avoid loss of events tokio::spawn( - PdfExportActor::new( + ExportActor::new( doc_rx.clone(), render_tx.subscribe(), - PdfExportConfig { + ExportConfig { substitute_pattern: self.config.output_path.clone(), entry: entry.clone(), mode: self.config.export_pdf, diff --git a/crates/tinymist/src/actor/render.rs b/crates/tinymist/src/actor/render.rs index 9d89826c..c9cb9311 100644 --- a/crates/tinymist/src/actor/render.rs +++ b/crates/tinymist/src/actor/render.rs @@ -7,52 +7,61 @@ use std::{ use anyhow::Context; use log::{error, info}; +use once_cell::sync::Lazy; use parking_lot::Mutex; +use tinymist_query::{ExportKind, PageSelection}; use tokio::sync::{ broadcast::{self, error::RecvError}, oneshot, watch, }; -use typst::foundations::Smart; +use typst::{foundations::Smart, layout::Frame}; use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument}; -use crate::ExportPdfMode; +use crate::ExportMode; + +#[derive(Debug, Clone)] +pub struct OneshotRendering { + pub kind: Option, + // todo: bad arch... + pub callback: Arc>>>>, +} #[derive(Debug, Clone)] pub enum RenderActorRequest { OnTyped, - // todo: bad arch... - DoExport(Arc>>>>), + Oneshot(OneshotRendering), OnSaved(PathBuf), - ChangeExportPath(PdfPathVars), - ChangeConfig(PdfExportConfig), + ChangeExportPath(PathVars), + ChangeConfig(ExportConfig), } #[derive(Debug, Clone)] -pub struct PdfPathVars { +pub struct PathVars { pub entry: EntryState, } #[derive(Debug, Clone, Default)] -pub struct PdfExportConfig { +pub struct ExportConfig { pub substitute_pattern: String, pub entry: EntryState, - pub mode: ExportPdfMode, + pub mode: ExportMode, } -pub struct PdfExportActor { +pub struct ExportActor { render_rx: broadcast::Receiver, document: watch::Receiver>>, pub substitute_pattern: String, pub entry: EntryState, - pub mode: ExportPdfMode, + pub mode: ExportMode, + pub kind: ExportKind, } -impl PdfExportActor { +impl ExportActor { pub fn new( document: watch::Receiver>>, render_rx: broadcast::Receiver, - config: PdfExportConfig, + config: ExportConfig, ) -> Self { Self { render_rx, @@ -60,27 +69,29 @@ impl PdfExportActor { substitute_pattern: config.substitute_pattern, entry: config.entry, mode: config.mode, + kind: ExportKind::Pdf, } } pub async fn run(mut self) { + let kind = &self.kind; loop { tokio::select! { req = self.render_rx.recv() => { let req = match req { Ok(req) => req, Err(RecvError::Closed) => { - info!("render actor channel closed"); + info!("RenderActor(@{kind:?}): channel closed"); break; } Err(RecvError::Lagged(_)) => { - info!("render actor channel lagged"); + info!("RenderActor(@{kind:?}): channel lagged"); continue; } }; - info!("PdfRenderActor: received request: {req:?}", req = req); + info!("RenderActor: received request: {req:?}", req = req); match req { RenderActorRequest::ChangeConfig(cfg) => { self.substitute_pattern = cfg.substitute_pattern; @@ -91,18 +102,18 @@ impl PdfExportActor { self.entry = cfg.entry; } _ => { - let sender = match &req { - RenderActorRequest::DoExport(sender) => Some(sender.clone()), + let cb = match &req { + RenderActorRequest::Oneshot(oneshot) => Some(oneshot.callback.clone()), _ => None, }; let resp = self.check_mode_and_export(req).await; - if let Some(sender) = sender { - let Some(sender) = sender.lock().take() else { - error!("PdfRenderActor: sender is None"); + if let Some(cb) = cb { + let Some(cb) = cb.lock().take() else { + error!("RenderActor(@{kind:?}): oneshot.callback is None"); continue; }; - if let Err(e) = sender.send(resp) { - error!("PdfRenderActor: failed to send response: {err:?}", err = e); + if let Err(e) = cb.send(resp) { + error!("RenderActor(@{kind:?}): failed to send response: {err:?}", err = e); } } } @@ -110,28 +121,35 @@ impl PdfExportActor { } } } - info!("PdfRenderActor: stopped"); + info!("RenderActor(@{kind:?}): stopped"); } async fn check_mode_and_export(&self, req: RenderActorRequest) -> Option { let Some(document) = self.document.borrow().clone() else { - info!("PdfRenderActor: document is not ready"); + info!("RenderActor: document is not ready"); return None; }; let eq_mode = match req { - RenderActorRequest::OnTyped => ExportPdfMode::OnType, - RenderActorRequest::DoExport(..) => ExportPdfMode::OnSave, - RenderActorRequest::OnSaved(..) => ExportPdfMode::OnSave, + RenderActorRequest::OnTyped => ExportMode::OnType, + RenderActorRequest::Oneshot(..) => ExportMode::OnSave, + RenderActorRequest::OnSaved(..) => ExportMode::OnSave, _ => unreachable!(), }; + let kind = if let RenderActorRequest::Oneshot(oneshot) = &req { + oneshot.kind.as_ref() + } else { + None + }; + let kind = kind.unwrap_or(&self.kind); + // pub entry: EntryState, let root = self.entry.root(); let main = self.entry.main(); info!( - "PdfRenderActor: check path {:?} and root {:?} with output directory {}", + "RenderActor: check path {:?} and root {:?} with output directory {}", main, root, self.substitute_pattern ); @@ -145,78 +163,131 @@ impl PdfExportActor { let path = main.vpath().resolve(&root)?; - let should_do = matches!(req, RenderActorRequest::DoExport(..)); + let should_do = matches!(req, RenderActorRequest::Oneshot(..)); let should_do = should_do || get_mode(self.mode) == eq_mode; - let should_do = should_do || validate_document(&req, self.mode, &document); + let should_do = should_do + || 'validate_doc: { + let mode = self.mode; + info!( + "RenderActor: validating document for export mode {mode:?} title is {title}", + title = document.title.is_some() + ); + if mode == ExportMode::OnDocumentHasTitle { + break 'validate_doc document.title.is_some() + && matches!(req, RenderActorRequest::OnSaved(..)); + } + + false + }; if should_do { - return match self.export_pdf(&document, &root, &path).await { + return match self.export(kind, &document, &root, &path).await { Ok(pdf) => Some(pdf), Err(err) => { - error!("PdfRenderActor: failed to export PDF: {err}", err = err); + error!("RenderActor({kind:?}): failed to export {err}", err = err); None } }; } - fn get_mode(mode: ExportPdfMode) -> ExportPdfMode { - if mode == ExportPdfMode::Auto { - return ExportPdfMode::Never; + fn get_mode(mode: ExportMode) -> ExportMode { + if mode == ExportMode::Auto { + return ExportMode::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 - } - None } - async fn export_pdf( + async fn export( &self, + kind: &ExportKind, doc: &TypstDocument, root: &Path, path: &Path, ) -> anyhow::Result { let Some(to) = substitute_path(&self.substitute_pattern, root, path) else { - return Err(anyhow::anyhow!("failed to substitute path")); + return Err(anyhow::anyhow!( + "RenderActor({kind:?}): failed to substitute path" + )); }; if to.is_relative() { - return Err(anyhow::anyhow!("path is relative: {to:?}")); + return Err(anyhow::anyhow!( + "RenderActor({kind:?}): path is relative: {to:?}" + )); } if to.is_dir() { - return Err(anyhow::anyhow!("path is a directory: {to:?}")); + return Err(anyhow::anyhow!( + "RenderActor({kind:?}): path is a directory: {to:?}" + )); } - let to = to.with_extension("pdf"); - info!("exporting PDF {path:?} to {to:?}"); + let to = to.with_extension(kind.extension()); + info!("RenderActor({kind:?}): exporting {path:?} to {to:?}"); if let Some(e) = to.parent() { if !e.exists() { - std::fs::create_dir_all(e).context("failed to create directory")?; + std::fs::create_dir_all(e).with_context(|| { + format!("RenderActor({kind:?}): failed to create directory") + })?; } } - // todo: Some(pdf_uri.as_str()) - // todo: timestamp world.now() - let data = typst_pdf::pdf(doc, Smart::Auto, None); + static DEFAULT_FRAME: Lazy = Lazy::new(Frame::default); + let data = match kind { + ExportKind::Pdf => { + // todo: Some(pdf_uri.as_str()) + // todo: timestamp world.now() + typst_pdf::pdf(doc, Smart::Auto, None) + } + ExportKind::Svg { + page: PageSelection::First, + } => typst_svg::svg( + doc.pages + .first() + .map(|f| &f.frame) + .unwrap_or(&*DEFAULT_FRAME), + ) + .into_bytes(), + ExportKind::Svg { + page: PageSelection::Merged, + } => typst_svg::svg_merged(doc, typst::layout::Abs::zero()).into_bytes(), + ExportKind::Png { + page: PageSelection::First, + } => { + let pixmap = typst_render::render( + doc.pages + .first() + .map(|f| &f.frame) + .unwrap_or(&*DEFAULT_FRAME), + 3., + typst::visualize::Color::WHITE, + ); + pixmap + .encode_png() + .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? + } + ExportKind::Png { + page: PageSelection::Merged, + } => { + let pixmap = typst_render::render_merged( + doc, + 3., + typst::visualize::Color::WHITE, + typst::layout::Abs::zero(), + typst::visualize::Color::WHITE, + ); + pixmap + .encode_png() + .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? + } + }; - std::fs::write(&to, data).context("failed to export PDF")?; + std::fs::write(&to, data) + .with_context(|| format!("RenderActor({kind:?}): failed to export"))?; - info!("PDF export complete"); + info!("RenderActor({kind:?}): export complete"); Ok(to) } } diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs index da789225..61e715c1 100644 --- a/crates/tinymist/src/actor/typ_client.rs +++ b/crates/tinymist/src/actor/typ_client.rs @@ -35,7 +35,7 @@ use log::{error, info, trace}; use parking_lot::Mutex; use tinymist_query::{ analysis::{Analysis, AnalysisContext, AnaylsisResources}, - CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature, + CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, ExportKind, FoldRequestFeature, OnExportRequest, OnSaveExportRequest, PositionEncoding, SemanticRequest, StatefulRequest, VersionedDocument, }; @@ -57,9 +57,9 @@ use typst_ts_core::{ }; use super::typ_server::CompileClient as TsCompileClient; -use super::{render::PdfExportConfig, typ_server::CompileServerActor}; +use super::{render::ExportConfig, typ_server::CompileServerActor}; use crate::{ - actor::render::{PdfPathVars, RenderActorRequest}, + actor::render::{OneshotRendering, PathVars, RenderActorRequest}, utils, }; use crate::{ @@ -317,7 +317,7 @@ impl CompileClientActor { ); self.render_tx - .send(RenderActorRequest::ChangeExportPath(PdfPathVars { + .send(RenderActorRequest::ChangeExportPath(PathVars { entry: next.clone(), })) .unwrap(); @@ -345,7 +345,7 @@ impl CompileClientActor { if res.is_err() { self.render_tx - .send(RenderActorRequest::ChangeExportPath(PdfPathVars { + .send(RenderActorRequest::ChangeExportPath(PathVars { entry: prev.clone(), })) .unwrap(); @@ -371,11 +371,11 @@ impl CompileClientActor { self.inner.wait().add_memory_changes(event); } - pub(crate) fn change_export_pdf(&self, config: PdfExportConfig) { + pub(crate) fn change_export_pdf(&self, config: ExportConfig) { let entry = self.entry.lock().clone(); let _ = self .render_tx - .send(RenderActorRequest::ChangeConfig(PdfExportConfig { + .send(RenderActorRequest::ChangeConfig(ExportConfig { substitute_pattern: config.substitute_pattern, // root: self.root.get().cloned().flatten(), entry, @@ -405,8 +405,8 @@ impl CompileClientActor { } match query { - CompilerQueryRequest::OnExport(OnExportRequest { path }) => { - Ok(CompilerQueryResponse::OnExport(self.on_export(path)?)) + CompilerQueryRequest::OnExport(OnExportRequest { kind, path }) => { + Ok(CompilerQueryResponse::OnExport(self.on_export(kind, path)?)) } CompilerQueryRequest::OnSaveExport(OnSaveExportRequest { path }) => { self.on_save_export(path)?; @@ -431,15 +431,18 @@ impl CompileClientActor { } } - fn on_export(&self, path: PathBuf) -> anyhow::Result> { + fn on_export(&self, kind: ExportKind, path: PathBuf) -> anyhow::Result> { + // todo: we currently doesn't respect the path argument... info!("CompileActor: on export: {}", path.display()); let (tx, rx) = oneshot::channel(); - let task = Arc::new(Mutex::new(Some(tx))); - + let callback = Arc::new(Mutex::new(Some(tx))); self.render_tx - .send(RenderActorRequest::DoExport(task)) + .send(RenderActorRequest::Oneshot(OneshotRendering { + kind: Some(kind), + callback, + })) .map_err(map_string_err("failed to send to sync_render"))?; let res: Option = utils::threaded_receive(rx)?; diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index 8769a246..cb5c5fe3 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -40,20 +40,19 @@ pub enum ExperimentalFormatterMode { Enable, } -/// The mode of PDF export. +/// The mode of PDF/SVG/PNG export. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] #[serde(rename_all = "camelCase")] -pub enum ExportPdfMode { +pub enum ExportMode { #[default] Auto, /// Select best solution automatically. (Recommended) Never, - /// Export PDF on saving the document, i.e. on `textDocument/didSave` - /// events. + /// Export on saving the document, i.e. on `textDocument/didSave` events. OnSave, - /// Export PDF on typing, i.e. on `textDocument/didChange` events. + /// Export on typing, i.e. on `textDocument/didChange` events. OnType, - /// Export PDFs when a document has a title, which is useful to filter out + /// Export when a document has a title, which is useful to filter out /// template files. OnDocumentHasTitle, } @@ -101,7 +100,7 @@ pub struct Config { /// The output directory for PDF export. pub output_path: String, /// The mode of PDF export. - pub export_pdf: ExportPdfMode, + pub export_pdf: ExportMode, /// Specifies the root path of the project manually. pub root_path: Option, /// Dynamic configuration for semantic tokens. @@ -207,12 +206,12 @@ impl Config { let export_pdf = update .get("exportPdf") - .map(ExportPdfMode::deserialize) + .map(ExportMode::deserialize) .and_then(Result::ok); if let Some(export_pdf) = export_pdf { self.export_pdf = export_pdf; } else { - self.export_pdf = ExportPdfMode::default(); + self.export_pdf = ExportMode::default(); } let root_path = update.get("rootPath"); @@ -683,7 +682,7 @@ mod tests { config.update(&update).unwrap(); assert_eq!(config.output_path, "out"); - assert_eq!(config.export_pdf, ExportPdfMode::OnSave); + assert_eq!(config.export_pdf, ExportMode::OnSave); assert_eq!(config.root_path, Some(PathBuf::from(root_path))); assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable); assert_eq!(config.formatter, ExperimentalFormatterMode::Enable); diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index 744af77a..2edecd9d 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -57,12 +57,13 @@ use lsp_types::request::{ use lsp_types::*; use parking_lot::{Mutex, RwLock}; use paste::paste; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as JsonValue}; use state::MemoryFileMeta; use tinymist_query::{ get_semantic_tokens_options, get_semantic_tokens_registration, - get_semantic_tokens_unregistration, DiagnosticsMap, SemanticTokenContext, + get_semantic_tokens_unregistration, DiagnosticsMap, ExportKind, PageSelection, + SemanticTokenContext, }; use tokio::sync::mpsc; use typst::diag::StrResult; @@ -75,7 +76,7 @@ pub type MaySyncResult<'a> = Result>; use world::SharedFontResolver; pub use world::{CompileFontOpts, CompileOnceOpts, CompileOpts}; -use crate::actor::render::PdfExportConfig; +use crate::actor::render::ExportConfig; use crate::init::*; use crate::tools::package::InitTask; @@ -665,6 +666,8 @@ impl TypstLanguageServer { ExecuteCmdMap::from_iter([ redirected_command!("tinymist.exportPdf", Self::export_pdf), + redirected_command!("tinymist.exportSvg", Self::export_svg), + redirected_command!("tinymist.exportPng", Self::export_png), redirected_command!("tinymist.doClearCache", Self::clear_cache), redirected_command!("tinymist.pinMain", Self::pin_document), redirected_command!("tinymist.focusMain", Self::focus_document), @@ -688,25 +691,29 @@ impl TypstLanguageServer { Ok(Some(handler(self, arguments)?)) } - /// Export the current document as a PDF file. The client is responsible for - /// passing the correct file URI. - /// - /// # Errors - /// Errors if a provided file URI is not a valid file URI. + /// Export the current document as a PDF file. pub fn export_pdf(&self, arguments: Vec) -> LspResult { - if arguments.is_empty() { - return Err(invalid_params("Missing file URI argument")); - } - let Some(file_uri) = arguments.first().and_then(|v| v.as_str()) else { - return Err(invalid_params("Missing file URI as first argument")); - }; - let file_uri = - Url::parse(file_uri).map_err(|_| invalid_params("Parameter is not a valid URI"))?; - let path = file_uri - .to_file_path() - .map_err(|_| invalid_params("URI is not a file URI"))?; + self.export(ExportKind::Pdf, arguments) + } - let res = run_query!(self.OnExport(path))?; + /// Export the current document as a Svg file. + pub fn export_svg(&self, arguments: Vec) -> LspResult { + let opts = parse_opts(arguments.get(1))?; + self.export(ExportKind::Svg { page: opts.page }, arguments) + } + + /// Export the current document as a Png file. + pub fn export_png(&self, arguments: Vec) -> LspResult { + let opts = parse_opts(arguments.get(1))?; + self.export(ExportKind::Png { page: opts.page }, arguments) + } + + /// Export the current document as some format. The client is responsible + /// for passing the correct absolute path of typst document. + pub fn export(&self, kind: ExportKind, arguments: Vec) -> LspResult { + let path = parse_path(arguments.first())?.as_ref().to_owned(); + + let res = run_query!(self.OnExport(path, kind))?; let res = serde_json::to_value(res).map_err(|_| internal_error("Cannot serialize path"))?; Ok(res) @@ -841,7 +848,22 @@ impl TypstLanguageServer { } } -fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult> { +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ExportOpts { + page: PageSelection, +} + +fn parse_opts(v: Option<&JsonValue>) -> LspResult { + Ok(match v { + Some(opts) => serde_json::from_value::(opts.clone()) + .map_err(|_| invalid_params("The third argument is not a valid object"))?, + _ => ExportOpts { + page: PageSelection::First, + }, + }) +} + +fn parse_path(v: Option<&JsonValue>) -> LspResult { let new_entry = match v { Some(JsonValue::String(s)) => { let s = Path::new(s); @@ -849,9 +871,8 @@ fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult> { return Err(invalid_params("entry should be absolute")); } - Some(s.into()) + s.into() } - Some(JsonValue::Null) => None, _ => { return Err(invalid_params( "The first parameter is not a valid path or null", @@ -862,6 +883,13 @@ fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult> { Ok(new_entry) } +fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult> { + match v { + Some(JsonValue::Null) => Ok(None), + v => Ok(Some(parse_path(v)?)), + } +} + /// Document Synchronization impl TypstLanguageServer { fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> { @@ -914,10 +942,10 @@ impl TypstLanguageServer { if config.output_path != self.config.output_path || config.export_pdf != self.config.export_pdf { - let config = PdfExportConfig { + let config = ExportConfig { substitute_pattern: self.config.output_path.clone(), mode: self.config.export_pdf, - ..PdfExportConfig::default() + ..ExportConfig::default() }; self.primary().change_export_pdf(config.clone()); diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 9f450dc5..33c1ac08 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -99,7 +99,7 @@ async function startClient(context: ExtensionContext): Promise { }); context.subscriptions.push( - commands.registerCommand("tinymist.exportCurrentPdf", commandExportCurrentPdf) + commands.registerCommand("tinymist.exportCurrentPdf", () => commandExport("Pdf")) ); context.subscriptions.push( commands.registerCommand("typst-lsp.pinMainToCurrent", () => commandPinMain(true)) @@ -107,7 +107,9 @@ async function startClient(context: ExtensionContext): Promise { context.subscriptions.push( commands.registerCommand("typst-lsp.unpinMain", () => commandPinMain(false)) ); - context.subscriptions.push(commands.registerCommand("tinymist.showPdf", commandShowPdf)); + context.subscriptions.push( + commands.registerCommand("tinymist.showPdf", () => commandShow("Pdf")) + ); context.subscriptions.push(commands.registerCommand("tinymist.clearCache", commandClearCache)); context.subscriptions.push( commands.registerCommand("tinymist.runCodeLens", commandRunCodeLens) @@ -195,19 +197,18 @@ function validateServer(path: string): { valid: true } | { valid: false; message } } -async function commandExportCurrentPdf(): Promise { +async function commandExport(mode: string, extraOpts?: any): Promise { const activeEditor = window.activeTextEditor; if (activeEditor === undefined) { return; } - const uri = activeEditor.document.uri.toString(); + const uri = activeEditor.document.uri.fsPath; const res = await client?.sendRequest("workspace/executeCommand", { - command: "tinymist.exportPdf", - arguments: [uri], + command: `tinymist.export${mode}`, + arguments: [uri, ...(extraOpts ? [extraOpts] : [])], }); - console.log("export pdf", res); if (res === null) { return undefined; } @@ -218,25 +219,25 @@ async function commandExportCurrentPdf(): Promise { * Implements the functionality for the 'Show PDF' button shown in the editor title * if a `.typ` file is opened. */ -async function commandShowPdf(): Promise { +async function commandShow(kind: string, extraOpts?: any): Promise { const activeEditor = window.activeTextEditor; if (activeEditor === undefined) { return; } // only create pdf if it does not exist yet - const pdfPath = await commandExportCurrentPdf(); + const exportPath = await commandExport(kind, extraOpts); - if (pdfPath === undefined) { + if (exportPath === undefined) { // show error message - await window.showErrorMessage("Failed to create PDF"); + await window.showErrorMessage("Failed to create"); return; } - const pdfUri = Uri.file(pdfPath); + const exportUri = Uri.file(exportPath); // here we can be sure that the pdf exists - await commands.executeCommand("vscode.open", pdfUri, ViewColumn.Beside); + await commands.executeCommand("vscode.open", exportUri, ViewColumn.Beside); } async function commandClearCache(): Promise { @@ -443,17 +444,43 @@ async function commandRunCodeLens(...args: string[]): Promise { break; } case "export-pdf": { - await commandShowPdf(); + await commandShow("Pdf"); break; } case "export-as": { - const fmt = await vscode.window.showQuickPick(["pdf"], { - title: "Format to export as", - }); - - if (fmt === "pdf") { - await commandShowPdf(); + enum FastKind { + PDF = "PDF", + SVG = "SVG (First Page)", + SVGMerged = "SVG (Merged)", + PNG = "PNG (First Page)", + PNGMerged = "PNG (Merged)", } + + const fmt = await vscode.window.showQuickPick( + [FastKind.PDF, FastKind.SVG, FastKind.SVGMerged, FastKind.PNG, FastKind.PNGMerged], + { + title: "Format to export as", + } + ); + + switch (fmt) { + case FastKind.PDF: + await commandShow("Pdf"); + break; + case FastKind.SVG: + await commandShow("Svg"); + break; + case FastKind.SVGMerged: + await commandShow("Svg", { page: "merged" }); + break; + case FastKind.PNG: + await commandShow("Png"); + break; + case FastKind.PNGMerged: + await commandShow("Png", { page: "merged" }); + break; + } + break; } default: {