diff --git a/Cargo.lock b/Cargo.lock index 5d141304..195dc8aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,15 @@ dependencies = [ "syn 2.0.11", ] +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs", +] + [[package]] name = "simplecss" version = "0.2.1" @@ -1632,6 +1641,7 @@ dependencies = [ "regex", "same-file", "serde_json", + "shellexpand", "siphasher", "tokio", "tower-lsp", diff --git a/Cargo.toml b/Cargo.toml index 6fadaf1b..7af94ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ parking_lot = "0.12.1" regex = "1.7.2" same-file = "1" serde_json = "1.0.94" +shellexpand = "3.1.0" siphasher = "0.3" tokio = { version = "1.26.0", features = [ "macros", diff --git a/addons/vscode/package.json b/addons/vscode/package.json index 179181cf..6a4a4896 100644 --- a/addons/vscode/package.json +++ b/addons/vscode/package.json @@ -35,6 +35,28 @@ "Export PDFs when you save a file.", "Export PDFs as you type in a file." ] + }, + "typst-lsp.outputRoot": { + "title": "Output Directory Root", + "description": "The directory that your output directory path is relative to.", + "type": "string", + "default": "source", + "enum": [ + "source", + "workspace", + "absolute" + ], + "enumDescriptions": [ + "The folder containing the source file", + "The VSCode workspace", + "The root of the filesystem" + ] + }, + "typst-lsp.outputPath": { + "title": "Output Directory Path", + "description": "The directory where to export PDFs. Relative to output directory root setting.", + "type": "string", + "default": "" } } }, diff --git a/src/command.rs b/src/command.rs index b8d25354..8cb0d6a9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -54,7 +54,15 @@ impl Backend { .map_err(|_| Error::invalid_params("Could not convert file URI to path"))?, ) .map_err(|_| Error::internal_error())?; - self.compile_diags_export(file_uri, text, true).await; + let config = self.config.read().await; + self.compile_diags_export( + file_uri, + text, + true, + config.output_root, + &config.output_path, + ) + .await; Ok(()) } } diff --git a/src/config.rs b/src/config.rs index 36bac734..a5a383bf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,22 @@ impl Default for ExportPdfMode { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputRoot { + Source, + Workspace, + Absolute, +} + +impl Default for OutputRoot { + fn default() -> Self { + Self::Source + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Config { pub export_pdf: ExportPdfMode, + pub output_root: OutputRoot, + pub output_path: String, } diff --git a/src/main.rs b/src/main.rs index 4f491c3d..dd0927f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,7 +198,30 @@ impl LanguageServer for Backend { _ => config::ExportPdfMode::OnSave, }) .unwrap_or_default(); + + let output_root = settings + .get("outputRoot") + .map(|val| match val { + JsonValue::String(val) => match val.as_str() { + "source" => config::OutputRoot::Source, + "workspace" => config::OutputRoot::Workspace, + "absolute" => config::OutputRoot::Absolute, + _ => config::OutputRoot::default(), + }, + _ => config::OutputRoot::default(), + }) + .unwrap_or_default(); + + let output_path = settings + .get("outputPath") + .and_then(|x| x.as_str()) + .unwrap_or_default() + .to_string(); + config.export_pdf = export_pdf; + config.output_root = output_root; + config.output_path = output_path; + self.client .log_message(MessageType::INFO, "New settings applied") .await; @@ -228,6 +251,8 @@ impl Backend { uri, text, matches!(config.export_pdf, config::ExportPdfMode::OnType), + config.output_root, + &config.output_path, ) .await; } @@ -238,11 +263,20 @@ impl Backend { uri, text, matches!(config.export_pdf, config::ExportPdfMode::OnSave), + config.output_root, + &config.output_path, ) .await; } - async fn compile_diags_export(&self, uri: Url, text: String, export: bool) { + async fn compile_diags_export( + &self, + file: Url, + text: String, + export: bool, + output_root: config::OutputRoot, + relative_dir: &str, + ) { let mut world_lock = self.world.write().await; let world = world_lock.as_mut().unwrap(); @@ -254,7 +288,7 @@ impl Backend { world.reset(); - match world.resolve_with(Path::new(&uri.to_file_path().unwrap()), &text) { + match world.resolve_with(Path::new(&file.to_file_path().unwrap()), &text) { Ok(id) => { world.main = id; } @@ -271,7 +305,44 @@ impl Backend { Ok(document) => { let buffer = typst::export::pdf(&document); if export { - let output_path = uri.to_file_path().unwrap().with_extension("pdf"); + let output_path: PathBuf = { + let root_dir = match output_root { + config::OutputRoot::Source => { + file.to_file_path().unwrap().parent().unwrap().to_path_buf() + } + config::OutputRoot::Workspace => world.root().to_path_buf(), + config::OutputRoot::Absolute => PathBuf::new(), + }; + + // expand tilde if necessary + let middle_dir = match output_root { + config::OutputRoot::Absolute => { + shellexpand::tilde(relative_dir).into_owned() + } + _ => relative_dir.to_string(), + }; + + let file_name = format!( + "{}.pdf", + file.to_file_path() + .unwrap() + .file_stem() + .unwrap() + .to_string_lossy() + ); + + [root_dir.to_str().unwrap(), &middle_dir, &file_name] + .iter() + .collect() + }; + + // create intermediate dirs if missing + if let Some(parent) = output_path.parent() { + // discard result because if this failed and was necessary, + // the save will fail and the error will be handled there + let _ = fs::create_dir_all(parent); + } + fs_message = match fs::write(&output_path, buffer) .map_err(|_| "failed to write PDF file".to_string()) { @@ -281,7 +352,7 @@ impl Backend { }), Err(e) => Some(LogMessage { message_type: MessageType::ERROR, - message: format!("{:?}", e), + message: format!("{:?} (writing to: {:?})", e, output_path), }), }; }