Skip to content

Commit 1e38e31

Browse files
feat: add typstExtraArgs
1 parent d00bf3e commit 1e38e31

File tree

3 files changed

+142
-5
lines changed

3 files changed

+142
-5
lines changed

crates/tinymist/src/actor.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ impl TypstLanguageServer {
2222
let (doc_tx, doc_rx) = watch::channel(None);
2323
let (render_tx, _) = broadcast::channel(10);
2424

25+
// todo: don't ignore entry from typst_extra_args
26+
// entry: command.input,
2527
let roots = self.roots.clone();
2628
let root_dir = self.config.root_path.clone();
27-
let root_dir = root_dir.unwrap_or_else(|| roots.first().cloned().unwrap_or_default());
29+
let root_dir = root_dir.or_else(|| {
30+
self.config
31+
.typst_extra_args
32+
.as_ref()
33+
.and_then(|x| x.root_dir.clone())
34+
});
35+
let root_dir = root_dir.unwrap_or_else(|| roots.first().cloned().unwrap());
2836
// Run the PDF export actor before preparing cluster to avoid loss of events
2937
tokio::spawn(
3038
PdfExportActor::new(
@@ -40,12 +48,26 @@ impl TypstLanguageServer {
4048
.run(),
4149
);
4250

43-
let opts = CompileOpts {
51+
let mut opts = CompileOpts {
4452
root_dir,
4553
// todo: additional inputs
4654
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
4755
..self.compile_opts.clone()
4856
};
57+
58+
if let Some(extras) = &self.config.typst_extra_args {
59+
if let Some(inputs) = extras.inputs.as_ref() {
60+
if opts.inputs.is_empty() {
61+
opts.inputs = inputs.clone();
62+
}
63+
}
64+
if !extras.font_paths.is_empty() && opts.font_paths.is_empty() {
65+
opts.font_paths = extras.font_paths.clone();
66+
}
67+
}
68+
69+
// ..self.config.typst_extra_args.clone()
70+
4971
create_server(
5072
name,
5173
self.const_config(),

crates/tinymist/src/init.rs

+106-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ use core::fmt;
22
use std::{collections::HashMap, path::PathBuf};
33

44
use anyhow::bail;
5+
use clap::builder::ValueParser;
6+
use clap::{ArgAction, Parser};
57
use itertools::Itertools;
6-
use log::info;
8+
use log::{error, info};
79
use lsp_types::*;
810
use serde::Deserialize;
911
use serde_json::{Map, Value as JsonValue};
1012
use tinymist_query::{get_semantic_tokens_options, PositionEncoding};
1113
use tokio::sync::mpsc;
14+
use typst::foundations::IntoValue;
1215
use typst_ts_core::config::CompileOpts;
16+
use typst_ts_core::TypstDict;
1317

1418
use crate::actor::cluster::CompileClusterActor;
1519
use crate::{invalid_params, LspHost, LspResult, TypstLanguageServer, TypstLanguageServerArgs};
@@ -141,6 +145,21 @@ pub enum SemanticTokensMode {
141145
Enable,
142146
}
143147

148+
#[derive(Debug, Clone, PartialEq, Default)]
149+
pub struct CompileExtraOpts {
150+
/// The root directory for compilation routine.
151+
pub root_dir: Option<PathBuf>,
152+
153+
/// Path to entry
154+
pub entry: Option<PathBuf>,
155+
156+
/// Additional input arguments to compile the entry file.
157+
pub inputs: Option<TypstDict>,
158+
159+
/// will remove later
160+
pub font_paths: Vec<PathBuf>,
161+
}
162+
144163
type Listener<T> = Box<dyn FnMut(&T) -> anyhow::Result<()>>;
145164

146165
const CONFIG_ITEMS: &[&str] = &[
@@ -149,6 +168,7 @@ const CONFIG_ITEMS: &[&str] = &[
149168
"rootPath",
150169
"semanticTokens",
151170
"experimentalFormatterMode",
171+
"typstExtraArgs",
152172
];
153173

154174
/// The user configuration read from the editor.
@@ -164,10 +184,53 @@ pub struct Config {
164184
pub semantic_tokens: SemanticTokensMode,
165185
/// Dynamic configuration for the experimental formatter.
166186
pub formatter: ExperimentalFormatterMode,
187+
/// Typst extra arguments.
188+
pub typst_extra_args: Option<CompileExtraOpts>,
167189
semantic_tokens_listeners: Vec<Listener<SemanticTokensMode>>,
168190
formatter_listeners: Vec<Listener<ExperimentalFormatterMode>>,
169191
}
170192

193+
/// Common arguments of compile, watch, and query.
194+
#[derive(Debug, Clone, Parser)]
195+
pub struct TypstArgs {
196+
/// Path to input Typst file, use `-` to read input from stdin
197+
#[clap(value_name = "INPUT")]
198+
pub input: Option<PathBuf>,
199+
200+
/// Configures the project root (for absolute paths)
201+
#[clap(long = "root", value_name = "DIR")]
202+
pub root: Option<PathBuf>,
203+
204+
/// Add a string key-value pair visible through `sys.inputs`
205+
#[clap(
206+
long = "input",
207+
value_name = "key=value",
208+
action = ArgAction::Append,
209+
value_parser = ValueParser::new(parse_input_pair),
210+
)]
211+
pub inputs: Vec<(String, String)>,
212+
213+
/// Adds additional directories to search for fonts
214+
#[clap(long = "font-path", value_name = "DIR")]
215+
pub font_paths: Vec<PathBuf>,
216+
}
217+
218+
/// Parses key/value pairs split by the first equal sign.
219+
///
220+
/// This function will return an error if the argument contains no equals sign
221+
/// or contains the key (before the equals sign) is empty.
222+
fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
223+
let (key, val) = raw
224+
.split_once('=')
225+
.ok_or("input must be a key and a value separated by an equal sign")?;
226+
let key = key.trim().to_owned();
227+
if key.is_empty() {
228+
return Err("the key was missing or empty".to_owned());
229+
}
230+
let val = val.trim().to_owned();
231+
Ok((key, val))
232+
}
233+
171234
impl Config {
172235
/// Gets items for serialization.
173236
pub fn get_items() -> Vec<ConfigurationItem> {
@@ -226,6 +289,8 @@ impl Config {
226289
.and_then(Result::ok);
227290
if let Some(export_pdf) = export_pdf {
228291
self.export_pdf = export_pdf;
292+
} else {
293+
self.export_pdf = ExportPdfMode::default();
229294
}
230295

231296
// todo: it doesn't respect the root path
@@ -237,6 +302,8 @@ impl Config {
237302
if let Some(root_path) = root_path.as_str().map(PathBuf::from) {
238303
self.root_path = Some(root_path);
239304
}
305+
} else {
306+
self.root_path = None;
240307
}
241308

242309
let semantic_tokens = update
@@ -261,6 +328,43 @@ impl Config {
261328
self.formatter = formatter;
262329
}
263330

331+
'parse_extra_args: {
332+
if let Some(typst_extra_args) = update.get("typstExtraArgs") {
333+
let typst_args: Vec<String> = match serde_json::from_value(typst_extra_args.clone())
334+
{
335+
Ok(e) => e,
336+
Err(e) => {
337+
error!("failed to parse typstExtraArgs: {e}");
338+
return Ok(());
339+
}
340+
};
341+
342+
let command = match TypstArgs::try_parse_from(
343+
Some("typst-cli".to_owned()).into_iter().chain(typst_args),
344+
) {
345+
Ok(e) => e,
346+
Err(e) => {
347+
error!("failed to parse typstExtraArgs: {e}");
348+
break 'parse_extra_args;
349+
}
350+
};
351+
352+
// Convert the input pairs to a dictionary.
353+
let inputs: TypstDict = command
354+
.inputs
355+
.iter()
356+
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
357+
.collect();
358+
359+
self.typst_extra_args = Some(CompileExtraOpts {
360+
entry: command.input,
361+
root_dir: command.root,
362+
inputs: Some(inputs),
363+
font_paths: command.font_paths,
364+
});
365+
}
366+
}
367+
264368
Ok(())
265369
}
266370

@@ -279,6 +383,7 @@ impl fmt::Debug for Config {
279383
.field("export_pdf", &self.export_pdf)
280384
.field("formatter", &self.formatter)
281385
.field("semantic_tokens", &self.semantic_tokens)
386+
.field("typst_extra_args", &self.typst_extra_args)
282387
.field(
283388
"semantic_tokens_listeners",
284389
&format_args!("Vec[len = {}]", self.semantic_tokens_listeners.len()),

editors/vscode/package.json

+12-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
]
7474
},
7575
"tinymist.noSystemFonts": {
76-
"title": "whether to load system fonts for Typst compiler",
76+
"title": "Whether to load system fonts for Typst compiler",
7777
"description": "A flag that determines whether to load system fonts for Typst compiler, which is useful for ensuring reproducible compilation. If set to null or not set, the extension will use the default behavior of the Typst compiler.",
7878
"type": [
7979
"boolean",
@@ -83,13 +83,23 @@
8383
},
8484
"tinymist.fontPaths": {
8585
"title": "Font paths for Typst compiler",
86-
"description": "Font paths, which doesn't allow for dynamic configuration",
86+
"description": "Font paths, which doesn't allow for dynamic configuration. Note: you can use vscode variables in the path, e.g. `${workspaceFolder}/fonts`.",
8787
"type": [
8888
"array",
8989
"null"
9090
],
9191
"default": null
9292
},
93+
"tinymist.typstExtraArgs": {
94+
"title": "Specifies the arguments for Typst as same as typst-cli",
95+
"description": "You can pass any arguments as you like, and we will try to follow behaviors of the **same version** of typst-cli. Note: the arguments may be overridden by other settings. For example, `--font-path` will be overridden by `tinymist.fontPaths`.",
96+
"type": "array",
97+
"items": {
98+
"type": "string",
99+
"title": "arguments in order"
100+
},
101+
"default": []
102+
},
93103
"tinymist.serverPath": {
94104
"title": "Path to server executable",
95105
"description": "The extension can use a local tinymist executable instead of the one bundled with the extension. This setting controls the path to the executable.",

0 commit comments

Comments
 (0)