feat: configure the output path of pdf files

This commit is contained in:
Myriad-Dreamin 2024-03-11 00:20:55 +08:00
parent 988b09ae0b
commit fc573db375
7 changed files with 168 additions and 36 deletions

View file

@ -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(),

View file

@ -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())
);
}
}

View file

@ -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)),

View file

@ -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() {

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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.",