mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: add svg and png export (#101)
This commit is contained in:
parent
e649ad308f
commit
413c2e8deb
9 changed files with 298 additions and 136 deletions
|
@ -43,6 +43,8 @@ codespan-reporting = "0.11"
|
||||||
typst = "0.11.0"
|
typst = "0.11.0"
|
||||||
typst-ide = "0.11.0"
|
typst-ide = "0.11.0"
|
||||||
typst-pdf = "0.11.0"
|
typst-pdf = "0.11.0"
|
||||||
|
typst-svg = "0.11.0"
|
||||||
|
typst-render = "0.11.0"
|
||||||
typst-assets = "0.11.0"
|
typst-assets = "0.11.0"
|
||||||
reflexo = { version = "0.5.0-rc2", default-features = false, features = [
|
reflexo = { version = "0.5.0-rc2", default-features = false, features = [
|
||||||
"flat-vector",
|
"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 = { 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-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-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" }
|
# typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" }
|
||||||
|
|
||||||
|
|
|
@ -109,12 +109,40 @@ pub trait StatefulRequest {
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
mod polymorphic {
|
mod polymorphic {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
use super::*;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OnExportRequest {
|
pub struct OnExportRequest {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub kind: ExportKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -36,7 +36,9 @@ clap_complete_fig.workspace = true
|
||||||
clap_mangen.workspace = true
|
clap_mangen.workspace = true
|
||||||
|
|
||||||
typst.workspace = true
|
typst.workspace = true
|
||||||
|
typst-svg.workspace = true
|
||||||
typst-pdf.workspace = true
|
typst-pdf.workspace = true
|
||||||
|
typst-render.workspace = true
|
||||||
typst-assets = { workspace = true, features = ["fonts"] }
|
typst-assets = { workspace = true, features = ["fonts"] }
|
||||||
|
|
||||||
typst-ts-core = { workspace = true, default-features = false, features = [
|
typst-ts-core = { workspace = true, default-features = false, features = [
|
||||||
|
|
|
@ -14,7 +14,7 @@ use typst_ts_compiler::{
|
||||||
use typst_ts_core::config::compiler::EntryState;
|
use typst_ts_core::config::compiler::EntryState;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
render::{PdfExportActor, PdfExportConfig},
|
render::{ExportActor, ExportConfig},
|
||||||
typ_client::{CompileClientActor, CompileDriver, CompileHandler},
|
typ_client::{CompileClientActor, CompileDriver, CompileHandler},
|
||||||
typ_server::CompileServerActor,
|
typ_server::CompileServerActor,
|
||||||
};
|
};
|
||||||
|
@ -30,12 +30,12 @@ impl TypstLanguageServer {
|
||||||
let (doc_tx, doc_rx) = watch::channel(None);
|
let (doc_tx, doc_rx) = watch::channel(None);
|
||||||
let (render_tx, _) = broadcast::channel(10);
|
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(
|
tokio::spawn(
|
||||||
PdfExportActor::new(
|
ExportActor::new(
|
||||||
doc_rx.clone(),
|
doc_rx.clone(),
|
||||||
render_tx.subscribe(),
|
render_tx.subscribe(),
|
||||||
PdfExportConfig {
|
ExportConfig {
|
||||||
substitute_pattern: self.config.output_path.clone(),
|
substitute_pattern: self.config.output_path.clone(),
|
||||||
entry: entry.clone(),
|
entry: entry.clone(),
|
||||||
mode: self.config.export_pdf,
|
mode: self.config.export_pdf,
|
||||||
|
|
|
@ -7,52 +7,61 @@ use std::{
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use tinymist_query::{ExportKind, PageSelection};
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
broadcast::{self, error::RecvError},
|
broadcast::{self, error::RecvError},
|
||||||
oneshot, watch,
|
oneshot, watch,
|
||||||
};
|
};
|
||||||
use typst::foundations::Smart;
|
use typst::{foundations::Smart, layout::Frame};
|
||||||
use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument};
|
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<ExportKind>,
|
||||||
|
// todo: bad arch...
|
||||||
|
pub callback: Arc<Mutex<Option<oneshot::Sender<Option<PathBuf>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum RenderActorRequest {
|
pub enum RenderActorRequest {
|
||||||
OnTyped,
|
OnTyped,
|
||||||
// todo: bad arch...
|
Oneshot(OneshotRendering),
|
||||||
DoExport(Arc<Mutex<Option<oneshot::Sender<Option<PathBuf>>>>>),
|
|
||||||
OnSaved(PathBuf),
|
OnSaved(PathBuf),
|
||||||
ChangeExportPath(PdfPathVars),
|
ChangeExportPath(PathVars),
|
||||||
ChangeConfig(PdfExportConfig),
|
ChangeConfig(ExportConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PdfPathVars {
|
pub struct PathVars {
|
||||||
pub entry: EntryState,
|
pub entry: EntryState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct PdfExportConfig {
|
pub struct ExportConfig {
|
||||||
pub substitute_pattern: String,
|
pub substitute_pattern: String,
|
||||||
pub entry: EntryState,
|
pub entry: EntryState,
|
||||||
pub mode: ExportPdfMode,
|
pub mode: ExportMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PdfExportActor {
|
pub struct ExportActor {
|
||||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||||
|
|
||||||
pub substitute_pattern: String,
|
pub substitute_pattern: String,
|
||||||
pub entry: EntryState,
|
pub entry: EntryState,
|
||||||
pub mode: ExportPdfMode,
|
pub mode: ExportMode,
|
||||||
|
pub kind: ExportKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PdfExportActor {
|
impl ExportActor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||||
config: PdfExportConfig,
|
config: ExportConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
render_rx,
|
render_rx,
|
||||||
|
@ -60,27 +69,29 @@ impl PdfExportActor {
|
||||||
substitute_pattern: config.substitute_pattern,
|
substitute_pattern: config.substitute_pattern,
|
||||||
entry: config.entry,
|
entry: config.entry,
|
||||||
mode: config.mode,
|
mode: config.mode,
|
||||||
|
kind: ExportKind::Pdf,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self) {
|
pub async fn run(mut self) {
|
||||||
|
let kind = &self.kind;
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
req = self.render_rx.recv() => {
|
req = self.render_rx.recv() => {
|
||||||
let req = match req {
|
let req = match req {
|
||||||
Ok(req) => req,
|
Ok(req) => req,
|
||||||
Err(RecvError::Closed) => {
|
Err(RecvError::Closed) => {
|
||||||
info!("render actor channel closed");
|
info!("RenderActor(@{kind:?}): channel closed");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(RecvError::Lagged(_)) => {
|
Err(RecvError::Lagged(_)) => {
|
||||||
info!("render actor channel lagged");
|
info!("RenderActor(@{kind:?}): channel lagged");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("PdfRenderActor: received request: {req:?}", req = req);
|
info!("RenderActor: received request: {req:?}", req = req);
|
||||||
match req {
|
match req {
|
||||||
RenderActorRequest::ChangeConfig(cfg) => {
|
RenderActorRequest::ChangeConfig(cfg) => {
|
||||||
self.substitute_pattern = cfg.substitute_pattern;
|
self.substitute_pattern = cfg.substitute_pattern;
|
||||||
|
@ -91,18 +102,18 @@ impl PdfExportActor {
|
||||||
self.entry = cfg.entry;
|
self.entry = cfg.entry;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let sender = match &req {
|
let cb = match &req {
|
||||||
RenderActorRequest::DoExport(sender) => Some(sender.clone()),
|
RenderActorRequest::Oneshot(oneshot) => Some(oneshot.callback.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let resp = self.check_mode_and_export(req).await;
|
let resp = self.check_mode_and_export(req).await;
|
||||||
if let Some(sender) = sender {
|
if let Some(cb) = cb {
|
||||||
let Some(sender) = sender.lock().take() else {
|
let Some(cb) = cb.lock().take() else {
|
||||||
error!("PdfRenderActor: sender is None");
|
error!("RenderActor(@{kind:?}): oneshot.callback is None");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Err(e) = sender.send(resp) {
|
if let Err(e) = cb.send(resp) {
|
||||||
error!("PdfRenderActor: failed to send response: {err:?}", err = e);
|
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<PathBuf> {
|
async fn check_mode_and_export(&self, req: RenderActorRequest) -> Option<PathBuf> {
|
||||||
let Some(document) = self.document.borrow().clone() else {
|
let Some(document) = self.document.borrow().clone() else {
|
||||||
info!("PdfRenderActor: document is not ready");
|
info!("RenderActor: document is not ready");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let eq_mode = match req {
|
let eq_mode = match req {
|
||||||
RenderActorRequest::OnTyped => ExportPdfMode::OnType,
|
RenderActorRequest::OnTyped => ExportMode::OnType,
|
||||||
RenderActorRequest::DoExport(..) => ExportPdfMode::OnSave,
|
RenderActorRequest::Oneshot(..) => ExportMode::OnSave,
|
||||||
RenderActorRequest::OnSaved(..) => ExportPdfMode::OnSave,
|
RenderActorRequest::OnSaved(..) => ExportMode::OnSave,
|
||||||
_ => unreachable!(),
|
_ => 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,
|
// pub entry: EntryState,
|
||||||
let root = self.entry.root();
|
let root = self.entry.root();
|
||||||
let main = self.entry.main();
|
let main = self.entry.main();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"PdfRenderActor: check path {:?} and root {:?} with output directory {}",
|
"RenderActor: check path {:?} and root {:?} with output directory {}",
|
||||||
main, root, self.substitute_pattern
|
main, root, self.substitute_pattern
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -145,78 +163,131 @@ impl PdfExportActor {
|
||||||
|
|
||||||
let path = main.vpath().resolve(&root)?;
|
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 || 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 {
|
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),
|
Ok(pdf) => Some(pdf),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("PdfRenderActor: failed to export PDF: {err}", err = err);
|
error!("RenderActor({kind:?}): failed to export {err}", err = err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_mode(mode: ExportPdfMode) -> ExportPdfMode {
|
fn get_mode(mode: ExportMode) -> ExportMode {
|
||||||
if mode == ExportPdfMode::Auto {
|
if mode == ExportMode::Auto {
|
||||||
return ExportPdfMode::Never;
|
return ExportMode::Never;
|
||||||
}
|
}
|
||||||
|
|
||||||
mode
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn export_pdf(
|
async fn export(
|
||||||
&self,
|
&self,
|
||||||
|
kind: &ExportKind,
|
||||||
doc: &TypstDocument,
|
doc: &TypstDocument,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<PathBuf> {
|
||||||
let Some(to) = substitute_path(&self.substitute_pattern, root, path) else {
|
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() {
|
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() {
|
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");
|
let to = to.with_extension(kind.extension());
|
||||||
info!("exporting PDF {path:?} to {to:?}");
|
info!("RenderActor({kind:?}): exporting {path:?} to {to:?}");
|
||||||
|
|
||||||
if let Some(e) = to.parent() {
|
if let Some(e) = to.parent() {
|
||||||
if !e.exists() {
|
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())
|
static DEFAULT_FRAME: Lazy<Frame> = Lazy::new(Frame::default);
|
||||||
// todo: timestamp world.now()
|
let data = match kind {
|
||||||
let data = typst_pdf::pdf(doc, Smart::Auto, None);
|
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)
|
Ok(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ use log::{error, info, trace};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use tinymist_query::{
|
use tinymist_query::{
|
||||||
analysis::{Analysis, AnalysisContext, AnaylsisResources},
|
analysis::{Analysis, AnalysisContext, AnaylsisResources},
|
||||||
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature,
|
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, ExportKind, FoldRequestFeature,
|
||||||
OnExportRequest, OnSaveExportRequest, PositionEncoding, SemanticRequest, StatefulRequest,
|
OnExportRequest, OnSaveExportRequest, PositionEncoding, SemanticRequest, StatefulRequest,
|
||||||
VersionedDocument,
|
VersionedDocument,
|
||||||
};
|
};
|
||||||
|
@ -57,9 +57,9 @@ use typst_ts_core::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::typ_server::CompileClient as TsCompileClient;
|
use super::typ_server::CompileClient as TsCompileClient;
|
||||||
use super::{render::PdfExportConfig, typ_server::CompileServerActor};
|
use super::{render::ExportConfig, typ_server::CompileServerActor};
|
||||||
use crate::{
|
use crate::{
|
||||||
actor::render::{PdfPathVars, RenderActorRequest},
|
actor::render::{OneshotRendering, PathVars, RenderActorRequest},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -317,7 +317,7 @@ impl CompileClientActor {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.render_tx
|
self.render_tx
|
||||||
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
|
.send(RenderActorRequest::ChangeExportPath(PathVars {
|
||||||
entry: next.clone(),
|
entry: next.clone(),
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -345,7 +345,7 @@ impl CompileClientActor {
|
||||||
|
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
self.render_tx
|
self.render_tx
|
||||||
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
|
.send(RenderActorRequest::ChangeExportPath(PathVars {
|
||||||
entry: prev.clone(),
|
entry: prev.clone(),
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -371,11 +371,11 @@ impl CompileClientActor {
|
||||||
self.inner.wait().add_memory_changes(event);
|
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 entry = self.entry.lock().clone();
|
||||||
let _ = self
|
let _ = self
|
||||||
.render_tx
|
.render_tx
|
||||||
.send(RenderActorRequest::ChangeConfig(PdfExportConfig {
|
.send(RenderActorRequest::ChangeConfig(ExportConfig {
|
||||||
substitute_pattern: config.substitute_pattern,
|
substitute_pattern: config.substitute_pattern,
|
||||||
// root: self.root.get().cloned().flatten(),
|
// root: self.root.get().cloned().flatten(),
|
||||||
entry,
|
entry,
|
||||||
|
@ -405,8 +405,8 @@ impl CompileClientActor {
|
||||||
}
|
}
|
||||||
|
|
||||||
match query {
|
match query {
|
||||||
CompilerQueryRequest::OnExport(OnExportRequest { path }) => {
|
CompilerQueryRequest::OnExport(OnExportRequest { kind, path }) => {
|
||||||
Ok(CompilerQueryResponse::OnExport(self.on_export(path)?))
|
Ok(CompilerQueryResponse::OnExport(self.on_export(kind, path)?))
|
||||||
}
|
}
|
||||||
CompilerQueryRequest::OnSaveExport(OnSaveExportRequest { path }) => {
|
CompilerQueryRequest::OnSaveExport(OnSaveExportRequest { path }) => {
|
||||||
self.on_save_export(path)?;
|
self.on_save_export(path)?;
|
||||||
|
@ -431,15 +431,18 @@ impl CompileClientActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_export(&self, path: PathBuf) -> anyhow::Result<Option<PathBuf>> {
|
fn on_export(&self, kind: ExportKind, path: PathBuf) -> anyhow::Result<Option<PathBuf>> {
|
||||||
|
// todo: we currently doesn't respect the path argument...
|
||||||
info!("CompileActor: on export: {}", path.display());
|
info!("CompileActor: on export: {}", path.display());
|
||||||
|
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let task = Arc::new(Mutex::new(Some(tx)));
|
let callback = Arc::new(Mutex::new(Some(tx)));
|
||||||
|
|
||||||
self.render_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"))?;
|
.map_err(map_string_err("failed to send to sync_render"))?;
|
||||||
|
|
||||||
let res: Option<PathBuf> = utils::threaded_receive(rx)?;
|
let res: Option<PathBuf> = utils::threaded_receive(rx)?;
|
||||||
|
|
|
@ -40,20 +40,19 @@ pub enum ExperimentalFormatterMode {
|
||||||
Enable,
|
Enable,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mode of PDF export.
|
/// The mode of PDF/SVG/PNG export.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum ExportPdfMode {
|
pub enum ExportMode {
|
||||||
#[default]
|
#[default]
|
||||||
Auto,
|
Auto,
|
||||||
/// Select best solution automatically. (Recommended)
|
/// Select best solution automatically. (Recommended)
|
||||||
Never,
|
Never,
|
||||||
/// Export PDF on saving the document, i.e. on `textDocument/didSave`
|
/// Export on saving the document, i.e. on `textDocument/didSave` events.
|
||||||
/// events.
|
|
||||||
OnSave,
|
OnSave,
|
||||||
/// Export PDF on typing, i.e. on `textDocument/didChange` events.
|
/// Export on typing, i.e. on `textDocument/didChange` events.
|
||||||
OnType,
|
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.
|
/// template files.
|
||||||
OnDocumentHasTitle,
|
OnDocumentHasTitle,
|
||||||
}
|
}
|
||||||
|
@ -101,7 +100,7 @@ pub struct Config {
|
||||||
/// The output directory for PDF export.
|
/// The output directory for PDF export.
|
||||||
pub output_path: String,
|
pub output_path: String,
|
||||||
/// The mode of PDF export.
|
/// The mode of PDF export.
|
||||||
pub export_pdf: ExportPdfMode,
|
pub export_pdf: ExportMode,
|
||||||
/// Specifies the root path of the project manually.
|
/// Specifies the root path of the project manually.
|
||||||
pub root_path: Option<PathBuf>,
|
pub root_path: Option<PathBuf>,
|
||||||
/// Dynamic configuration for semantic tokens.
|
/// Dynamic configuration for semantic tokens.
|
||||||
|
@ -207,12 +206,12 @@ impl Config {
|
||||||
|
|
||||||
let export_pdf = update
|
let export_pdf = update
|
||||||
.get("exportPdf")
|
.get("exportPdf")
|
||||||
.map(ExportPdfMode::deserialize)
|
.map(ExportMode::deserialize)
|
||||||
.and_then(Result::ok);
|
.and_then(Result::ok);
|
||||||
if let Some(export_pdf) = export_pdf {
|
if let Some(export_pdf) = export_pdf {
|
||||||
self.export_pdf = export_pdf;
|
self.export_pdf = export_pdf;
|
||||||
} else {
|
} else {
|
||||||
self.export_pdf = ExportPdfMode::default();
|
self.export_pdf = ExportMode::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let root_path = update.get("rootPath");
|
let root_path = update.get("rootPath");
|
||||||
|
@ -683,7 +682,7 @@ mod tests {
|
||||||
config.update(&update).unwrap();
|
config.update(&update).unwrap();
|
||||||
|
|
||||||
assert_eq!(config.output_path, "out");
|
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.root_path, Some(PathBuf::from(root_path)));
|
||||||
assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
|
assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
|
||||||
assert_eq!(config.formatter, ExperimentalFormatterMode::Enable);
|
assert_eq!(config.formatter, ExperimentalFormatterMode::Enable);
|
||||||
|
|
|
@ -57,12 +57,13 @@ use lsp_types::request::{
|
||||||
use lsp_types::*;
|
use lsp_types::*;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::{Map, Value as JsonValue};
|
||||||
use state::MemoryFileMeta;
|
use state::MemoryFileMeta;
|
||||||
use tinymist_query::{
|
use tinymist_query::{
|
||||||
get_semantic_tokens_options, get_semantic_tokens_registration,
|
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 tokio::sync::mpsc;
|
||||||
use typst::diag::StrResult;
|
use typst::diag::StrResult;
|
||||||
|
@ -75,7 +76,7 @@ pub type MaySyncResult<'a> = Result<JsonValue, BoxFuture<'a, JsonValue>>;
|
||||||
use world::SharedFontResolver;
|
use world::SharedFontResolver;
|
||||||
pub use world::{CompileFontOpts, CompileOnceOpts, CompileOpts};
|
pub use world::{CompileFontOpts, CompileOnceOpts, CompileOpts};
|
||||||
|
|
||||||
use crate::actor::render::PdfExportConfig;
|
use crate::actor::render::ExportConfig;
|
||||||
use crate::init::*;
|
use crate::init::*;
|
||||||
use crate::tools::package::InitTask;
|
use crate::tools::package::InitTask;
|
||||||
|
|
||||||
|
@ -665,6 +666,8 @@ impl TypstLanguageServer {
|
||||||
|
|
||||||
ExecuteCmdMap::from_iter([
|
ExecuteCmdMap::from_iter([
|
||||||
redirected_command!("tinymist.exportPdf", Self::export_pdf),
|
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.doClearCache", Self::clear_cache),
|
||||||
redirected_command!("tinymist.pinMain", Self::pin_document),
|
redirected_command!("tinymist.pinMain", Self::pin_document),
|
||||||
redirected_command!("tinymist.focusMain", Self::focus_document),
|
redirected_command!("tinymist.focusMain", Self::focus_document),
|
||||||
|
@ -688,25 +691,29 @@ impl TypstLanguageServer {
|
||||||
Ok(Some(handler(self, arguments)?))
|
Ok(Some(handler(self, arguments)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export the current document as a PDF file. The client is responsible for
|
/// Export the current document as a PDF file.
|
||||||
/// passing the correct file URI.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Errors if a provided file URI is not a valid file URI.
|
|
||||||
pub fn export_pdf(&self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
|
pub fn export_pdf(&self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
|
||||||
if arguments.is_empty() {
|
self.export(ExportKind::Pdf, arguments)
|
||||||
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"))?;
|
|
||||||
|
|
||||||
let res = run_query!(self.OnExport(path))?;
|
/// Export the current document as a Svg file.
|
||||||
|
pub fn export_svg(&self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
|
||||||
|
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<JsonValue>) -> LspResult<JsonValue> {
|
||||||
|
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<JsonValue>) -> LspResult<JsonValue> {
|
||||||
|
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"))?;
|
let res = serde_json::to_value(res).map_err(|_| internal_error("Cannot serialize path"))?;
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
|
@ -841,7 +848,22 @@ impl TypstLanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult<Option<ImmutPath>> {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ExportOpts {
|
||||||
|
page: PageSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_opts(v: Option<&JsonValue>) -> LspResult<ExportOpts> {
|
||||||
|
Ok(match v {
|
||||||
|
Some(opts) => serde_json::from_value::<ExportOpts>(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<ImmutPath> {
|
||||||
let new_entry = match v {
|
let new_entry = match v {
|
||||||
Some(JsonValue::String(s)) => {
|
Some(JsonValue::String(s)) => {
|
||||||
let s = Path::new(s);
|
let s = Path::new(s);
|
||||||
|
@ -849,9 +871,8 @@ fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult<Option<ImmutPath>> {
|
||||||
return Err(invalid_params("entry should be absolute"));
|
return Err(invalid_params("entry should be absolute"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(s.into())
|
s.into()
|
||||||
}
|
}
|
||||||
Some(JsonValue::Null) => None,
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(invalid_params(
|
return Err(invalid_params(
|
||||||
"The first parameter is not a valid path or null",
|
"The first parameter is not a valid path or null",
|
||||||
|
@ -862,6 +883,13 @@ fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult<Option<ImmutPath>> {
|
||||||
Ok(new_entry)
|
Ok(new_entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_path_or_null(v: Option<&JsonValue>) -> LspResult<Option<ImmutPath>> {
|
||||||
|
match v {
|
||||||
|
Some(JsonValue::Null) => Ok(None),
|
||||||
|
v => Ok(Some(parse_path(v)?)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Document Synchronization
|
/// Document Synchronization
|
||||||
impl TypstLanguageServer {
|
impl TypstLanguageServer {
|
||||||
fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> {
|
fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> {
|
||||||
|
@ -914,10 +942,10 @@ impl TypstLanguageServer {
|
||||||
if config.output_path != self.config.output_path
|
if config.output_path != self.config.output_path
|
||||||
|| config.export_pdf != self.config.export_pdf
|
|| config.export_pdf != self.config.export_pdf
|
||||||
{
|
{
|
||||||
let config = PdfExportConfig {
|
let config = ExportConfig {
|
||||||
substitute_pattern: self.config.output_path.clone(),
|
substitute_pattern: self.config.output_path.clone(),
|
||||||
mode: self.config.export_pdf,
|
mode: self.config.export_pdf,
|
||||||
..PdfExportConfig::default()
|
..ExportConfig::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.primary().change_export_pdf(config.clone());
|
self.primary().change_export_pdf(config.clone());
|
||||||
|
|
|
@ -99,7 +99,7 @@ async function startClient(context: ExtensionContext): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
commands.registerCommand("tinymist.exportCurrentPdf", commandExportCurrentPdf)
|
commands.registerCommand("tinymist.exportCurrentPdf", () => commandExport("Pdf"))
|
||||||
);
|
);
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
commands.registerCommand("typst-lsp.pinMainToCurrent", () => commandPinMain(true))
|
commands.registerCommand("typst-lsp.pinMainToCurrent", () => commandPinMain(true))
|
||||||
|
@ -107,7 +107,9 @@ async function startClient(context: ExtensionContext): Promise<void> {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
commands.registerCommand("typst-lsp.unpinMain", () => commandPinMain(false))
|
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.clearCache", commandClearCache));
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
commands.registerCommand("tinymist.runCodeLens", commandRunCodeLens)
|
commands.registerCommand("tinymist.runCodeLens", commandRunCodeLens)
|
||||||
|
@ -195,19 +197,18 @@ function validateServer(path: string): { valid: true } | { valid: false; message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function commandExportCurrentPdf(): Promise<string | undefined> {
|
async function commandExport(mode: string, extraOpts?: any): Promise<string | undefined> {
|
||||||
const activeEditor = window.activeTextEditor;
|
const activeEditor = window.activeTextEditor;
|
||||||
if (activeEditor === undefined) {
|
if (activeEditor === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = activeEditor.document.uri.toString();
|
const uri = activeEditor.document.uri.fsPath;
|
||||||
|
|
||||||
const res = await client?.sendRequest<string | null>("workspace/executeCommand", {
|
const res = await client?.sendRequest<string | null>("workspace/executeCommand", {
|
||||||
command: "tinymist.exportPdf",
|
command: `tinymist.export${mode}`,
|
||||||
arguments: [uri],
|
arguments: [uri, ...(extraOpts ? [extraOpts] : [])],
|
||||||
});
|
});
|
||||||
console.log("export pdf", res);
|
|
||||||
if (res === null) {
|
if (res === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -218,25 +219,25 @@ async function commandExportCurrentPdf(): Promise<string | undefined> {
|
||||||
* Implements the functionality for the 'Show PDF' button shown in the editor title
|
* Implements the functionality for the 'Show PDF' button shown in the editor title
|
||||||
* if a `.typ` file is opened.
|
* if a `.typ` file is opened.
|
||||||
*/
|
*/
|
||||||
async function commandShowPdf(): Promise<void> {
|
async function commandShow(kind: string, extraOpts?: any): Promise<void> {
|
||||||
const activeEditor = window.activeTextEditor;
|
const activeEditor = window.activeTextEditor;
|
||||||
if (activeEditor === undefined) {
|
if (activeEditor === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only create pdf if it does not exist yet
|
// 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
|
// show error message
|
||||||
await window.showErrorMessage("Failed to create PDF");
|
await window.showErrorMessage("Failed to create");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfUri = Uri.file(pdfPath);
|
const exportUri = Uri.file(exportPath);
|
||||||
|
|
||||||
// here we can be sure that the pdf exists
|
// 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<void> {
|
async function commandClearCache(): Promise<void> {
|
||||||
|
@ -443,17 +444,43 @@ async function commandRunCodeLens(...args: string[]): Promise<void> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "export-pdf": {
|
case "export-pdf": {
|
||||||
await commandShowPdf();
|
await commandShow("Pdf");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "export-as": {
|
case "export-as": {
|
||||||
const fmt = await vscode.window.showQuickPick(["pdf"], {
|
enum FastKind {
|
||||||
title: "Format to export as",
|
PDF = "PDF",
|
||||||
});
|
SVG = "SVG (First Page)",
|
||||||
|
SVGMerged = "SVG (Merged)",
|
||||||
if (fmt === "pdf") {
|
PNG = "PNG (First Page)",
|
||||||
await commandShowPdf();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue