Skip to content

Commit

Permalink
feat: add typstExtraArgs
Browse files Browse the repository at this point in the history
  • Loading branch information
Myriad-Dreamin committed Mar 10, 2024
1 parent d00bf3e commit 1e38e31
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 5 deletions.
26 changes: 24 additions & 2 deletions crates/tinymist/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ impl TypstLanguageServer {
let (doc_tx, doc_rx) = watch::channel(None);
let (render_tx, _) = broadcast::channel(10);

// todo: don't ignore entry from typst_extra_args
// entry: command.input,
let roots = self.roots.clone();
let root_dir = self.config.root_path.clone();
let root_dir = root_dir.unwrap_or_else(|| roots.first().cloned().unwrap_or_default());
let root_dir = root_dir.or_else(|| {
self.config
.typst_extra_args
.as_ref()
.and_then(|x| x.root_dir.clone())
});
let root_dir = root_dir.unwrap_or_else(|| roots.first().cloned().unwrap());
// Run the PDF export actor before preparing cluster to avoid loss of events
tokio::spawn(
PdfExportActor::new(
Expand All @@ -40,12 +48,26 @@ impl TypstLanguageServer {
.run(),
);

let opts = CompileOpts {
let mut opts = CompileOpts {
root_dir,
// todo: additional inputs
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
..self.compile_opts.clone()
};

if let Some(extras) = &self.config.typst_extra_args {
if let Some(inputs) = extras.inputs.as_ref() {
if opts.inputs.is_empty() {
opts.inputs = inputs.clone();
}
}
if !extras.font_paths.is_empty() && opts.font_paths.is_empty() {
opts.font_paths = extras.font_paths.clone();
}
}

// ..self.config.typst_extra_args.clone()

create_server(
name,
self.const_config(),
Expand Down
107 changes: 106 additions & 1 deletion crates/tinymist/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ use core::fmt;
use std::{collections::HashMap, path::PathBuf};

use anyhow::bail;
use clap::builder::ValueParser;
use clap::{ArgAction, Parser};
use itertools::Itertools;
use log::info;
use log::{error, info};
use lsp_types::*;
use serde::Deserialize;
use serde_json::{Map, Value as JsonValue};
use tinymist_query::{get_semantic_tokens_options, PositionEncoding};
use tokio::sync::mpsc;
use typst::foundations::IntoValue;
use typst_ts_core::config::CompileOpts;
use typst_ts_core::TypstDict;

use crate::actor::cluster::CompileClusterActor;
use crate::{invalid_params, LspHost, LspResult, TypstLanguageServer, TypstLanguageServerArgs};
Expand Down Expand Up @@ -141,6 +145,21 @@ pub enum SemanticTokensMode {
Enable,
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct CompileExtraOpts {
/// The root directory for compilation routine.
pub root_dir: Option<PathBuf>,

/// Path to entry
pub entry: Option<PathBuf>,

/// Additional input arguments to compile the entry file.
pub inputs: Option<TypstDict>,

/// will remove later
pub font_paths: Vec<PathBuf>,
}

type Listener<T> = Box<dyn FnMut(&T) -> anyhow::Result<()>>;

const CONFIG_ITEMS: &[&str] = &[
Expand All @@ -149,6 +168,7 @@ const CONFIG_ITEMS: &[&str] = &[
"rootPath",
"semanticTokens",
"experimentalFormatterMode",
"typstExtraArgs",
];

/// The user configuration read from the editor.
Expand All @@ -164,10 +184,53 @@ pub struct Config {
pub semantic_tokens: SemanticTokensMode,
/// Dynamic configuration for the experimental formatter.
pub formatter: ExperimentalFormatterMode,
/// Typst extra arguments.
pub typst_extra_args: Option<CompileExtraOpts>,
semantic_tokens_listeners: Vec<Listener<SemanticTokensMode>>,
formatter_listeners: Vec<Listener<ExperimentalFormatterMode>>,
}

/// Common arguments of compile, watch, and query.
#[derive(Debug, Clone, Parser)]
pub struct TypstArgs {
/// Path to input Typst file, use `-` to read input from stdin
#[clap(value_name = "INPUT")]
pub input: Option<PathBuf>,

/// Configures the project root (for absolute paths)
#[clap(long = "root", value_name = "DIR")]
pub root: Option<PathBuf>,

/// Add a string key-value pair visible through `sys.inputs`
#[clap(
long = "input",
value_name = "key=value",
action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair),
)]
pub inputs: Vec<(String, String)>,

/// Adds additional directories to search for fonts
#[clap(long = "font-path", value_name = "DIR")]
pub font_paths: Vec<PathBuf>,
}

/// Parses key/value pairs split by the first equal sign.
///
/// This function will return an error if the argument contains no equals sign
/// or contains the key (before the equals sign) is empty.
fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
let (key, val) = raw
.split_once('=')
.ok_or("input must be a key and a value separated by an equal sign")?;
let key = key.trim().to_owned();
if key.is_empty() {
return Err("the key was missing or empty".to_owned());
}
let val = val.trim().to_owned();
Ok((key, val))
}

impl Config {
/// Gets items for serialization.
pub fn get_items() -> Vec<ConfigurationItem> {
Expand Down Expand Up @@ -226,6 +289,8 @@ impl Config {
.and_then(Result::ok);
if let Some(export_pdf) = export_pdf {
self.export_pdf = export_pdf;
} else {
self.export_pdf = ExportPdfMode::default();
}

// todo: it doesn't respect the root path
Expand All @@ -237,6 +302,8 @@ impl Config {
if let Some(root_path) = root_path.as_str().map(PathBuf::from) {
self.root_path = Some(root_path);
}
} else {
self.root_path = None;
}

let semantic_tokens = update
Expand All @@ -261,6 +328,43 @@ impl Config {
self.formatter = formatter;
}

'parse_extra_args: {
if let Some(typst_extra_args) = update.get("typstExtraArgs") {
let typst_args: Vec<String> = match serde_json::from_value(typst_extra_args.clone())
{
Ok(e) => e,
Err(e) => {
error!("failed to parse typstExtraArgs: {e}");
return Ok(());
}
};

let command = match TypstArgs::try_parse_from(
Some("typst-cli".to_owned()).into_iter().chain(typst_args),
) {
Ok(e) => e,
Err(e) => {
error!("failed to parse typstExtraArgs: {e}");
break 'parse_extra_args;
}
};

// Convert the input pairs to a dictionary.
let inputs: TypstDict = command
.inputs
.iter()
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
.collect();

self.typst_extra_args = Some(CompileExtraOpts {
entry: command.input,
root_dir: command.root,
inputs: Some(inputs),
font_paths: command.font_paths,
});
}
}

Ok(())
}

Expand All @@ -279,6 +383,7 @@ impl fmt::Debug for Config {
.field("export_pdf", &self.export_pdf)
.field("formatter", &self.formatter)
.field("semantic_tokens", &self.semantic_tokens)
.field("typst_extra_args", &self.typst_extra_args)
.field(
"semantic_tokens_listeners",
&format_args!("Vec[len = {}]", self.semantic_tokens_listeners.len()),
Expand Down
14 changes: 12 additions & 2 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
]
},
"tinymist.noSystemFonts": {
"title": "whether to load system fonts for Typst compiler",
"title": "Whether to load system fonts for Typst compiler",
"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.",
"type": [
"boolean",
Expand All @@ -83,13 +83,23 @@
},
"tinymist.fontPaths": {
"title": "Font paths for Typst compiler",
"description": "Font paths, which doesn't allow for dynamic configuration",
"description": "Font paths, which doesn't allow for dynamic configuration. Note: you can use vscode variables in the path, e.g. `${workspaceFolder}/fonts`.",
"type": [
"array",
"null"
],
"default": null
},
"tinymist.typstExtraArgs": {
"title": "Specifies the arguments for Typst as same as typst-cli",
"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`.",
"type": "array",
"items": {
"type": "string",
"title": "arguments in order"
},
"default": []
},
"tinymist.serverPath": {
"title": "Path to server executable",
"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.",
Expand Down

0 comments on commit 1e38e31

Please sign in to comment.