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-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" }
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ExportKind>,
|
||||
// todo: bad arch...
|
||||
pub callback: Arc<Mutex<Option<oneshot::Sender<Option<PathBuf>>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RenderActorRequest {
|
||||
OnTyped,
|
||||
// todo: bad arch...
|
||||
DoExport(Arc<Mutex<Option<oneshot::Sender<Option<PathBuf>>>>>),
|
||||
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<RenderActorRequest>,
|
||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||
|
||||
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<Option<Arc<TypstDocument>>>,
|
||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
static DEFAULT_FRAME: Lazy<Frame> = Lazy::new(Frame::default);
|
||||
let data = match kind {
|
||||
ExportKind::Pdf => {
|
||||
// todo: Some(pdf_uri.as_str())
|
||||
// todo: timestamp world.now()
|
||||
let data = typst_pdf::pdf(doc, Smart::Auto, None);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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());
|
||||
|
||||
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<PathBuf> = utils::threaded_receive(rx)?;
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
/// 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);
|
||||
|
|
|
@ -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<JsonValue, BoxFuture<'a, JsonValue>>;
|
|||
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<JsonValue>) -> LspResult<JsonValue> {
|
||||
if arguments.is_empty() {
|
||||
return Err(invalid_params("Missing file URI argument"));
|
||||
self.export(ExportKind::Pdf, arguments)
|
||||
}
|
||||
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"))?;
|
||||
|
||||
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 {
|
||||
Some(JsonValue::String(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"));
|
||||
}
|
||||
|
||||
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<Option<ImmutPath>> {
|
|||
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
|
||||
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());
|
||||
|
|
|
@ -99,7 +99,7 @@ async function startClient(context: ExtensionContext): Promise<void> {
|
|||
});
|
||||
|
||||
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<void> {
|
|||
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<string | undefined> {
|
||||
async function commandExport(mode: string, extraOpts?: any): Promise<string | undefined> {
|
||||
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<string | null>("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<string | undefined> {
|
|||
* Implements the functionality for the 'Show PDF' button shown in the editor title
|
||||
* if a `.typ` file is opened.
|
||||
*/
|
||||
async function commandShowPdf(): Promise<void> {
|
||||
async function commandShow(kind: string, extraOpts?: any): Promise<void> {
|
||||
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<void> {
|
||||
|
@ -443,17 +444,43 @@ async function commandRunCodeLens(...args: string[]): Promise<void> {
|
|||
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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue