mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 09:52:27 +00:00
feat: configure the output path of pdf files
This commit is contained in:
parent
988b09ae0b
commit
fc573db375
7 changed files with 168 additions and 36 deletions
|
@ -22,24 +22,25 @@ impl TypstLanguageServer {
|
|||
let (doc_tx, doc_rx) = watch::channel(None);
|
||||
let (render_tx, _) = broadcast::channel(10);
|
||||
|
||||
let roots = self.roots.clone();
|
||||
let root_dir = roots.first().cloned().unwrap_or_default();
|
||||
// Run the PDF export actor before preparing cluster to avoid loss of events
|
||||
tokio::spawn(
|
||||
PdfExportActor::new(
|
||||
doc_rx.clone(),
|
||||
render_tx.subscribe(),
|
||||
Some(PdfExportConfig {
|
||||
path: entry
|
||||
.as_ref()
|
||||
.map(|e| e.clone().with_extension("pdf").into()),
|
||||
PdfExportConfig {
|
||||
substitute_pattern: self.config.output_path.clone(),
|
||||
root: root_dir.clone().into(),
|
||||
path: entry.clone().map(From::from),
|
||||
mode: self.config.export_pdf,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.run(),
|
||||
);
|
||||
|
||||
let roots = self.roots.clone();
|
||||
let opts = CompileOpts {
|
||||
root_dir: roots.first().cloned().unwrap_or_default(),
|
||||
root_dir,
|
||||
// todo: font paths
|
||||
// font_paths: arguments.font_paths.clone(),
|
||||
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
|
||||
|
|
|
@ -12,7 +12,7 @@ use tokio::sync::{
|
|||
watch,
|
||||
};
|
||||
use typst::foundations::Smart;
|
||||
use typst_ts_core::{ImmutPath, TypstDocument};
|
||||
use typst_ts_core::{path::PathClean, ImmutPath, TypstDocument};
|
||||
|
||||
use crate::ExportPdfMode;
|
||||
|
||||
|
@ -20,12 +20,20 @@ use crate::ExportPdfMode;
|
|||
pub enum RenderActorRequest {
|
||||
OnTyped,
|
||||
OnSaved(PathBuf),
|
||||
ChangeExportPath(Option<ImmutPath>),
|
||||
ChangeExportPath(PdfPathVars),
|
||||
ChangeConfig(PdfExportConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PdfPathVars {
|
||||
pub root: ImmutPath,
|
||||
pub path: Option<ImmutPath>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PdfExportConfig {
|
||||
pub substitute_pattern: String,
|
||||
pub root: ImmutPath,
|
||||
pub path: Option<ImmutPath>,
|
||||
pub mode: ExportPdfMode,
|
||||
}
|
||||
|
@ -34,6 +42,8 @@ pub struct PdfExportActor {
|
|||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||
|
||||
pub substitute_pattern: String,
|
||||
pub root: ImmutPath,
|
||||
pub path: Option<ImmutPath>,
|
||||
pub mode: ExportPdfMode,
|
||||
}
|
||||
|
@ -42,13 +52,15 @@ impl PdfExportActor {
|
|||
pub fn new(
|
||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||
config: Option<PdfExportConfig>,
|
||||
config: PdfExportConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
render_rx,
|
||||
document,
|
||||
path: config.as_ref().and_then(|c| c.path.clone()),
|
||||
mode: config.map(|c| c.mode).unwrap_or(ExportPdfMode::Auto),
|
||||
substitute_pattern: config.substitute_pattern,
|
||||
root: config.root,
|
||||
path: config.path,
|
||||
mode: config.mode,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,11 +84,14 @@ impl PdfExportActor {
|
|||
info!("PdfRenderActor: received request: {req:?}", req = req);
|
||||
match req {
|
||||
RenderActorRequest::ChangeConfig(cfg) => {
|
||||
self.substitute_pattern = cfg.substitute_pattern;
|
||||
self.root = cfg.root;
|
||||
self.path = cfg.path;
|
||||
self.mode = cfg.mode;
|
||||
}
|
||||
RenderActorRequest::ChangeExportPath(cfg) => {
|
||||
self.path = cfg;
|
||||
self.root = cfg.root;
|
||||
self.path = cfg.path;
|
||||
}
|
||||
_ => {
|
||||
self.check_mode_and_export(req).await;
|
||||
|
@ -99,7 +114,10 @@ impl PdfExportActor {
|
|||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
info!("PdfRenderActor: check path {:?}", self.path);
|
||||
info!(
|
||||
"PdfRenderActor: check path {:?} with output directory {}",
|
||||
self.path, self.substitute_pattern
|
||||
);
|
||||
if let Some(path) = self.path.as_ref() {
|
||||
if (get_mode(self.mode) == eq_mode) || validate_document(&req, self.mode, &document) {
|
||||
let Err(err) = self.export_pdf(&document, path).await else {
|
||||
|
@ -135,15 +153,84 @@ impl PdfExportActor {
|
|||
}
|
||||
|
||||
async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<()> {
|
||||
let Some(to) = substitute_path(&self.substitute_pattern, &self.root, path) else {
|
||||
return Err(anyhow::anyhow!("failed to substitute path"));
|
||||
};
|
||||
if to.is_relative() {
|
||||
return Err(anyhow::anyhow!("path is relative: {to:?}"));
|
||||
}
|
||||
if to.is_dir() {
|
||||
return Err(anyhow::anyhow!("path is a directory: {to:?}"));
|
||||
}
|
||||
|
||||
let to = to.with_extension("pdf");
|
||||
info!("exporting PDF {path:?} to {to:?}");
|
||||
|
||||
if let Some(e) = to.parent() {
|
||||
if !e.exists() {
|
||||
std::fs::create_dir_all(e).context("failed to create directory")?;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: Some(pdf_uri.as_str())
|
||||
// todo: timestamp world.now()
|
||||
info!("exporting PDF {path}", path = path.display());
|
||||
|
||||
let data = typst_pdf::pdf(doc, Smart::Auto, None);
|
||||
|
||||
std::fs::write(path, data).context("failed to export PDF")?;
|
||||
std::fs::write(to, data).context("failed to export PDF")?;
|
||||
|
||||
info!("PDF export complete");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn substitute_path(substitute_pattern: &str, root: &Path, path: &Path) -> Option<ImmutPath> {
|
||||
if substitute_pattern.is_empty() {
|
||||
return Some(path.to_path_buf().clean().into());
|
||||
}
|
||||
|
||||
let path = path.strip_prefix(root).ok()?;
|
||||
let dir = path.parent();
|
||||
let file_name = path.file_name().unwrap_or_default();
|
||||
|
||||
let w = root.to_string_lossy();
|
||||
let f = file_name.to_string_lossy();
|
||||
|
||||
// replace all $root
|
||||
let mut path = substitute_pattern.replace("$root", &w);
|
||||
if let Some(dir) = dir {
|
||||
let d = dir.to_string_lossy();
|
||||
path = path.replace("$dir", &d);
|
||||
}
|
||||
path = path.replace("$name", &f);
|
||||
|
||||
Some(PathBuf::from(path).clean().into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_substitute_path() {
|
||||
let root = Path::new("/root");
|
||||
let path = Path::new("/root/dir1/dir2/file.txt");
|
||||
|
||||
assert_eq!(
|
||||
substitute_path("/substitute/$dir/$name", root, path),
|
||||
Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
|
||||
);
|
||||
assert_eq!(
|
||||
substitute_path("/substitute/$dir/../$name", root, path),
|
||||
Some(PathBuf::from("/substitute/dir1/file.txt").into())
|
||||
);
|
||||
assert_eq!(
|
||||
substitute_path("/substitute/$name", root, path),
|
||||
Some(PathBuf::from("/substitute/file.txt").into())
|
||||
);
|
||||
assert_eq!(
|
||||
substitute_path("/substitute/target/$dir/$name", root, path),
|
||||
Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ use typst_ts_core::{
|
|||
|
||||
use super::compile::CompileClient as TsCompileClient;
|
||||
use super::{compile::CompileActor as CompileActorInner, render::PdfExportConfig};
|
||||
use crate::actor::render::RenderActorRequest;
|
||||
use crate::actor::render::{PdfPathVars, RenderActorRequest};
|
||||
use crate::ConstConfig;
|
||||
|
||||
type CompileService<H> = CompileActorInner<Reporter<CompileExporter<CompileDriver>, H>>;
|
||||
|
@ -80,7 +80,7 @@ pub fn create_server(
|
|||
inner: driver,
|
||||
cb: handler.clone(),
|
||||
};
|
||||
let driver = CompileActorInner::new(driver, root).with_watch(true);
|
||||
let driver = CompileActorInner::new(driver, root.clone()).with_watch(true);
|
||||
|
||||
let (server, client) = driver.split();
|
||||
|
||||
|
@ -88,6 +88,7 @@ pub fn create_server(
|
|||
|
||||
let this = CompileActor::new(
|
||||
diag_group,
|
||||
root.into(),
|
||||
cfg.position_encoding,
|
||||
handler,
|
||||
client,
|
||||
|
@ -297,6 +298,7 @@ pub struct CompileActor {
|
|||
diag_group: String,
|
||||
position_encoding: PositionEncoding,
|
||||
handler: CompileHandler,
|
||||
root: ImmutPath,
|
||||
entry: Arc<Mutex<Option<ImmutPath>>>,
|
||||
pub inner: CompileClient<CompileHandler>,
|
||||
render_tx: broadcast::Sender<RenderActorRequest>,
|
||||
|
@ -383,9 +385,10 @@ impl CompileActor {
|
|||
);
|
||||
|
||||
self.render_tx
|
||||
.send(RenderActorRequest::ChangeExportPath(Some(
|
||||
next.with_extension("pdf").into(),
|
||||
)))
|
||||
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
|
||||
root: self.root.clone(),
|
||||
path: Some(next.clone()),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
// todo
|
||||
|
@ -402,9 +405,10 @@ impl CompileActor {
|
|||
|
||||
if res.is_err() {
|
||||
self.render_tx
|
||||
.send(RenderActorRequest::ChangeExportPath(
|
||||
prev.clone().map(|e| e.with_extension("pdf").into()),
|
||||
))
|
||||
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
|
||||
root: self.root.clone(),
|
||||
path: prev.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let mut entry = entry.lock();
|
||||
|
@ -423,7 +427,7 @@ impl CompileActor {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn change_export_pdf(&self, export_pdf: crate::ExportPdfMode) {
|
||||
pub(crate) fn change_export_pdf(&self, config: PdfExportConfig) {
|
||||
let entry = self.entry.lock();
|
||||
let path = entry
|
||||
.as_ref()
|
||||
|
@ -431,8 +435,10 @@ impl CompileActor {
|
|||
let _ = self
|
||||
.render_tx
|
||||
.send(RenderActorRequest::ChangeConfig(PdfExportConfig {
|
||||
substitute_pattern: config.substitute_pattern,
|
||||
root: self.root.clone(),
|
||||
path,
|
||||
mode: export_pdf,
|
||||
mode: config.mode,
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -526,6 +532,7 @@ impl CompileHost for CompileActor {}
|
|||
impl CompileActor {
|
||||
fn new(
|
||||
diag_group: String,
|
||||
root: ImmutPath,
|
||||
position_encoding: PositionEncoding,
|
||||
handler: CompileHandler,
|
||||
inner: CompileClient<CompileHandler>,
|
||||
|
@ -533,6 +540,7 @@ impl CompileActor {
|
|||
) -> Self {
|
||||
Self {
|
||||
diag_group,
|
||||
root,
|
||||
position_encoding,
|
||||
handler,
|
||||
entry: Arc::new(Mutex::new(None)),
|
||||
|
|
|
@ -143,6 +143,7 @@ pub enum SemanticTokensMode {
|
|||
type Listener<T> = Box<dyn FnMut(&T) -> anyhow::Result<()>>;
|
||||
|
||||
const CONFIG_ITEMS: &[&str] = &[
|
||||
"outputPath",
|
||||
"exportPdf",
|
||||
"rootPath",
|
||||
"semanticTokens",
|
||||
|
@ -152,6 +153,8 @@ const CONFIG_ITEMS: &[&str] = &[
|
|||
/// The user configuration read from the editor.
|
||||
#[derive(Default)]
|
||||
pub struct Config {
|
||||
/// The output directory for PDF export.
|
||||
pub output_path: String,
|
||||
/// The mode of PDF export.
|
||||
pub export_pdf: ExportPdfMode,
|
||||
/// Specifies the root path of the project manually.
|
||||
|
@ -210,6 +213,12 @@ impl Config {
|
|||
/// # Errors
|
||||
/// Errors if the update is invalid.
|
||||
pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> anyhow::Result<()> {
|
||||
if let Some(JsonValue::String(output_path)) = update.get("outputPath") {
|
||||
self.output_path = output_path.to_owned();
|
||||
} else {
|
||||
self.output_path = String::new();
|
||||
}
|
||||
|
||||
let export_pdf = update
|
||||
.get("exportPdf")
|
||||
.map(ExportPdfMode::deserialize)
|
||||
|
@ -218,6 +227,7 @@ impl Config {
|
|||
self.export_pdf = export_pdf;
|
||||
}
|
||||
|
||||
// todo: it doesn't respect the root path
|
||||
let root_path = update.get("rootPath");
|
||||
if let Some(root_path) = root_path {
|
||||
if root_path.is_null() {
|
||||
|
|
|
@ -62,6 +62,7 @@ use typst::util::Deferred;
|
|||
|
||||
pub type MaySyncResult<'a> = Result<JsonValue, BoxFuture<'a, JsonValue>>;
|
||||
|
||||
use crate::actor::render::PdfExportConfig;
|
||||
use crate::init::*;
|
||||
|
||||
// Enforces drop order
|
||||
|
@ -793,17 +794,27 @@ impl TypstLanguageServer {
|
|||
}
|
||||
|
||||
fn on_changed_configuration(&mut self, values: Map<String, JsonValue>) -> LspResult<()> {
|
||||
let output_directory = self.config.output_path.clone();
|
||||
let export_pdf = self.config.export_pdf;
|
||||
match self.config.update_by_map(&values) {
|
||||
Ok(()) => {
|
||||
info!("new settings applied");
|
||||
|
||||
if export_pdf != self.config.export_pdf {
|
||||
self.primary().change_export_pdf(self.config.export_pdf);
|
||||
if output_directory != self.config.output_path
|
||||
|| export_pdf != self.config.export_pdf
|
||||
{
|
||||
let config = PdfExportConfig {
|
||||
substitute_pattern: self.config.output_path.clone(),
|
||||
mode: self.config.export_pdf,
|
||||
root: Path::new("").into(),
|
||||
path: None,
|
||||
};
|
||||
|
||||
self.primary().change_export_pdf(config.clone());
|
||||
{
|
||||
let m = self.main.lock();
|
||||
if let Some(main) = m.as_ref() {
|
||||
main.wait().change_export_pdf(self.config.export_pdf);
|
||||
main.wait().change_export_pdf(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,23 @@ See [Tinymist features](../../README.md#features) for a list of features.
|
|||
the output as you work, install a PDF viewer extension, such as
|
||||
`vscode-pdf`.
|
||||
- To configure when PDFs are compiled:
|
||||
1. Open settings
|
||||
1. Open settings.
|
||||
- File -> Preferences -> Settings (Linux, Windows)
|
||||
- Code -> Preferences -> Settings (Mac)
|
||||
2. Search for "Typst Export PDF"
|
||||
3. Change the Export PDF setting
|
||||
- `onSave` makes a PDF after saving the Typst file
|
||||
- `onType` makes PDF files live, as you type
|
||||
- `never` disables PDF compilation
|
||||
2. Search for "Tinymist Export PDF".
|
||||
3. Change the Export PDF setting.
|
||||
- `onSave` makes a PDF after saving the Typst file.
|
||||
- `onType` makes PDF files live, as you type.
|
||||
- `never` disables PDF compilation.
|
||||
- "onDocumentHasTitle" makes a PDF when the document has a title and, as you save.
|
||||
- To configure where PDFs are saved:
|
||||
1. Open settings.
|
||||
2. Search for "Tinymist Output Path".
|
||||
3. Change the Export PDF setting. This is the path pattern to store artifacts, you can use `$root` or `$dir` or `$name` to do magic configuration
|
||||
- e.g. `$root/$dir/$name` (default) for `$root/path/to/main.pdf`.
|
||||
- e.g. `$root/target/$dir/$name` for `$root/target/path/to/main.pdf`.
|
||||
- e.g. `$root/foo` for `$root/foo.pdf`.
|
||||
4. Note: the output path should be substituted as an absolute path.
|
||||
|
||||
## Technical
|
||||
|
||||
|
|
|
@ -23,6 +23,12 @@
|
|||
"type": "object",
|
||||
"title": "Tinymist Typst LSP",
|
||||
"properties": {
|
||||
"tinymist.outputPath": {
|
||||
"title": "Output path",
|
||||
"description": "The path pattern to store Typst artifacts, you can use `$root` or `$dir` or `$name` to do magic configuration, e.g. `$dir/$name` (default) and `$root/target/$dir/$name`.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"tinymist.exportPdf": {
|
||||
"title": "Export PDF",
|
||||
"description": "The extension can export PDFs of your Typst files. This setting controls whether this feature is enabled and how often it runs.",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue