1use core::fmt;
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, LazyLock, OnceLock};
4
5use clap::Parser;
6use itertools::Itertools;
7use lsp_types::*;
8use reflexo::error::IgnoreLogging;
9use reflexo::CowStr;
10use reflexo_typst::{ImmutPath, TypstDict};
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value as JsonValue};
13use strum::IntoEnumIterator;
14use task::{FormatUserConfig, FormatterConfig};
15use tinymist_l10n::DebugL10n;
16use tinymist_project::{DynAccessModel, LspAccessModel};
17use tinymist_query::analysis::{Modifier, TokenType};
18use tinymist_query::{url_to_path, CompletionFeat, PositionEncoding};
19use tinymist_render::PeriscopeArgs;
20use tinymist_std::error::prelude::*;
21use tinymist_task::ExportTarget;
22use typst::foundations::IntoValue;
23use typst::Features;
24use typst_shim::utils::LazyHash;
25
26use super::*;
27use crate::input::WatchAccessModel;
28use crate::project::{
29 EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen,
30};
31use crate::world::font::FontResolverImpl;
32
33#[cfg(feature = "export")]
34use task::ExportUserConfig;
35#[cfg(feature = "preview")]
36use tinymist_preview::{PreviewConfig, PreviewInvertColors};
37
38#[cfg(feature = "export")]
39use crate::project::{ExportPdfTask, ProjectTask};
40
41const CONFIG_ITEMS: &[&str] = &[
43 "tinymist",
44 "colorTheme",
45 "compileStatus",
46 "lint",
47 "completion",
48 "development",
49 "exportPdf",
50 "exportTarget",
51 "fontPaths",
52 "formatterMode",
53 "formatterPrintWidth",
54 "formatterIndentSize",
55 "formatterProseWrap",
56 "hoverPeriscope",
57 "onEnter",
58 "outputPath",
59 "preview",
60 "projectResolution",
61 "rootPath",
62 "semanticTokens",
63 "systemFonts",
64 "typstExtraArgs",
65];
66#[derive(Debug, Default, Clone)]
74pub struct Config {
75 pub const_config: ConstConfig,
77 pub const_dap_config: ConstDapConfig,
79
80 pub delegate_fs_requests: bool,
82 pub customized_show_document: bool,
84 pub has_default_entry_path: bool,
86 pub notify_status: bool,
88 pub support_html_in_markdown: bool,
90 pub extended_code_action: bool,
93 pub development: bool,
95
96 pub color_theme: Option<String>,
98 pub entry_resolver: EntryResolver,
100 pub lsp_inputs: ImmutDict,
102 pub periscope_args: Option<PeriscopeArgs>,
104 pub typst_extra_args: Option<TypstExtraArgs>,
106 pub semantic_tokens: SemanticTokensMode,
108
109 pub completion: CompletionFeat,
111 pub preview: PreviewFeat,
113 pub lint: LintFeat,
115 pub on_enter: OnEnterFeat,
117
118 pub font_opts: CompileFontArgs,
120 pub font_paths: Vec<PathBuf>,
122 pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
124 pub system_fonts: Option<bool>,
126
127 pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
129 pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
131
132 pub export_target: ExportTarget,
134 pub export_pdf: TaskWhen,
136 pub output_path: PathPattern,
138
139 pub formatter_mode: FormatterMode,
141 pub formatter_print_width: Option<u32>,
144 pub formatter_indent_size: Option<u32>,
146 pub formatter_prose_wrap: Option<bool>,
148 pub warnings: Vec<CowStr>,
150}
151
152impl Config {
153 pub fn new(
155 const_config: ConstConfig,
156 roots: Vec<ImmutPath>,
157 font_opts: CompileFontArgs,
158 ) -> Self {
159 let mut config = Self {
160 const_config,
161 const_dap_config: ConstDapConfig::default(),
162 entry_resolver: EntryResolver {
163 roots,
164 ..EntryResolver::default()
165 },
166 font_opts,
167 ..Self::default()
168 };
169 config
170 .update_by_map(&Map::default())
171 .log_error("failed to assign Config defaults");
172 config
173 }
174
175 pub fn extract_lsp_params(
181 params: InitializeParams,
182 font_args: CompileFontArgs,
183 ) -> (Self, Option<ResponseError>) {
184 let roots = match params.workspace_folders.as_ref() {
186 Some(roots) => roots
187 .iter()
188 .map(|root| ImmutPath::from(url_to_path(&root.uri)))
189 .collect(),
190 #[allow(deprecated)] None => params
192 .root_uri
193 .as_ref()
194 .map(|uri| ImmutPath::from(url_to_path(uri)))
195 .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into()))
196 .into_iter()
197 .collect(),
198 };
199 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
200
201 if let Some(locale) = config.const_config.locale.as_ref() {
203 tinymist_l10n::set_locale(locale);
204 }
205
206 let err = params
207 .initialization_options
208 .and_then(|init| config.update(&init).map_err(invalid_params).err());
209
210 (config, err)
211 }
212
213 pub fn extract_dap_params(
219 params: dapts::InitializeRequestArguments,
220 font_args: CompileFontArgs,
221 ) -> (Self, Option<ResponseError>) {
222 let cwd = std::env::current_dir()
224 .expect("failed to get current directory")
225 .into();
226
227 let roots = vec![cwd];
229 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
230 config.const_dap_config = ConstDapConfig::from(¶ms);
231
232 if let Some(locale) = config.const_config.locale.as_ref() {
234 tinymist_l10n::set_locale(locale);
235 }
236
237 (config, None)
238 }
239
240 pub fn get_items() -> Vec<ConfigurationItem> {
243 CONFIG_ITEMS
244 .iter()
245 .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
246 .map(|section| ConfigurationItem {
247 section: Some(section),
248 ..ConfigurationItem::default()
249 })
250 .collect()
251 }
252
253 pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
255 let unpaired_values = values
256 .into_iter()
257 .tuples()
258 .map(|(a, b)| if !a.is_null() { a } else { b });
259
260 CONFIG_ITEMS
261 .iter()
262 .map(|&item| item.to_owned())
263 .zip(unpaired_values)
264 .collect()
265 }
266
267 pub fn update(&mut self, update: &JsonValue) -> Result<()> {
272 if let JsonValue::Object(update) = update {
273 self.update_by_map(update)?;
274
275 if let Some(namespaced) = update.get("tinymist").and_then(JsonValue::as_object) {
277 self.update_by_map(namespaced)?;
278 }
279
280 Ok(())
281 } else {
282 tinymist_l10n::bail!(
283 "tinymist.config.invalidObject",
284 "invalid configuration object: {object}",
285 object = update.debug_l10n(),
286 )
287 }
288 }
289
290 pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> Result<()> {
295 log::info!(
296 "ServerState: config update_by_map {}",
297 serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
298 );
299
300 self.warnings.clear();
301
302 macro_rules! try_deserialize {
303 ($ty:ty, $key:expr) => {
304 update.get($key).and_then(|v| {
305 <$ty>::deserialize(v)
306 .inspect_err(|err| {
307 if v.is_null() {
310 return;
311 }
312
313 self.warnings.push(tinymist_l10n::t!(
314 "tinymist.config.deserializeError",
315 "failed to deserialize \"{key}\": {err}",
316 key = $key.debug_l10n(),
317 err = err.debug_l10n(),
318 ));
319 })
320 .ok()
321 })
322 };
323 }
324
325 macro_rules! assign_config {
326 ($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
327 let v = try_deserialize!($ty, $bind);
328 self.$($field_path).+ = v.unwrap_or_default();
329 };
330 ($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
331 let v = try_deserialize!($ty, $bind);
332 self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
333 };
334 }
335
336 assign_config!(color_theme := "colorTheme"?: Option<String>);
337 assign_config!(lint := "lint"?: LintFeat);
338 assign_config!(completion := "completion"?: CompletionFeat);
339 assign_config!(on_enter := "onEnter"?: OnEnterFeat);
340 assign_config!(completion.trigger_suggest := "triggerSuggest"?: bool);
341 assign_config!(completion.trigger_parameter_hints := "triggerParameterHints"?: bool);
342 assign_config!(completion.trigger_suggest_and_parameter_hints := "triggerSuggestAndParameterHints"?: bool);
343 assign_config!(customized_show_document := "customizedShowDocument"?: bool);
344 assign_config!(entry_resolver.project_resolution := "projectResolution"?: ProjectResolutionKind);
345 assign_config!(export_pdf := "exportPdf"?: TaskWhen);
346 assign_config!(export_target := "exportTarget"?: ExportTarget);
347 assign_config!(font_paths := "fontPaths"?: Vec<_>);
348 assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
349 assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
350 assign_config!(formatter_indent_size := "formatterIndentSize"?: Option<u32>);
351 assign_config!(formatter_prose_wrap := "formatterProseWrap"?: Option<bool>);
352 assign_config!(output_path := "outputPath"?: PathPattern);
353 assign_config!(preview := "preview"?: PreviewFeat);
354 assign_config!(lint := "lint"?: LintFeat);
355 assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
356 assign_config!(delegate_fs_requests := "delegateFsRequests"?: bool);
357 assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
358 assign_config!(extended_code_action := "supportExtendedCodeAction"?: bool);
359 assign_config!(development := "development"?: bool);
360 assign_config!(system_fonts := "systemFonts"?: Option<bool>);
361
362 self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
363 Some("enable") => true,
364 Some("disable") | None => false,
365 Some(value) => {
366 self.warnings.push(tinymist_l10n::t!(
367 "tinymist.config.badCompileStatus",
368 "compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
369 value = value.debug_l10n(),
370 ));
371
372 false
373 }
374 };
375
376 self.periscope_args = match update.get("hoverPeriscope") {
378 Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()),
379 Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None,
380 Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
381 Ok(args) => Some(args),
382 Err(err) => {
383 self.warnings.push(tinymist_l10n::t!(
384 "tinymist.config.badHoverPeriscope",
385 "failed to parse hoverPeriscope: {err}",
386 err = err.debug_l10n(),
387 ));
388 None
389 }
390 },
391 };
392 if let Some(args) = self.periscope_args.as_mut() {
393 if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") {
394 "always".clone_into(&mut args.invert_color);
395 }
396 }
397
398 fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> CowStr {
399 log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
400 tinymist_l10n::t!(
401 "tinymist.config.badTypstExtraArgs",
402 "failed to parse typstExtraArgs: {err}, args: {args}",
403 err = err.debug_l10n(),
404 args = args.debug_l10n(),
405 )
406 }
407
408 {
409 let raw_args = || update.get("typstExtraArgs");
410 let typst_args: Vec<String> = match raw_args().cloned().map(serde_json::from_value) {
411 Some(Ok(args)) => args,
412 Some(Err(err)) => {
413 self.warnings.push(invalid_extra_args(&raw_args(), err));
414 None
415 }
416 None => None,
419 }
420 .unwrap_or_default();
421 let empty_typst_args = typst_args.is_empty();
422
423 let args = match CompileOnceArgs::try_parse_from(
424 Some("typst-cli".to_owned()).into_iter().chain(typst_args),
425 ) {
426 Ok(args) => args,
427 Err(err) => {
428 self.warnings.push(invalid_extra_args(&raw_args(), err));
429
430 if empty_typst_args {
431 CompileOnceArgs::default()
432 } else {
433 CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
435 .inspect_err(|err| {
436 log::error!("failed to make default typstExtraArgs: {err}");
437 })
438 .unwrap_or_default()
439 }
440 }
441 };
442
443 self.typst_extra_args = Some(TypstExtraArgs {
445 inputs: args.resolve_inputs().unwrap_or_default(),
446 entry: args.input.map(|e| Path::new(&e).into()),
447 root_dir: args.root.as_ref().map(|r| r.as_path().into()),
448 font: args.font,
449 package: args.package,
450 pdf_standard: args.pdf.standard,
451 no_pdf_tags: args.pdf.no_tags,
452 ppi: args.png.ppi,
453 features: args.features,
454 creation_timestamp: args.creation_timestamp,
455 cert: args.cert.as_deref().map(From::from),
456 });
457 }
458
459 self.entry_resolver.root_path =
460 try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
461 self.typst_extra_args
462 .as_ref()
463 .and_then(|e| e.root_dir.clone())
464 });
465 self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
466 self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
467 self.lsp_inputs = {
468 let mut dict = TypstDict::default();
469
470 #[derive(Serialize)]
471 #[serde(rename_all = "camelCase")]
472 struct PreviewInputs {
473 pub version: u32,
474 pub theme: String,
475 }
476
477 dict.insert(
478 "x-preview".into(),
479 serde_json::to_string(&PreviewInputs {
480 version: 1,
481 theme: self.color_theme.clone().unwrap_or_default(),
482 })
483 .unwrap()
484 .into_value(),
485 );
486
487 Arc::new(LazyHash::new(dict))
488 };
489
490 self.validate()
491 }
492
493 pub fn validate(&self) -> Result<()> {
495 self.entry_resolver.validate()?;
496
497 Ok(())
498 }
499
500 pub fn formatter(&self) -> FormatUserConfig {
502 let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
503 let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
504 let formatter_line_wrap = self.formatter_prose_wrap.unwrap_or(false);
505
506 FormatUserConfig {
507 config: match self.formatter_mode {
508 FormatterMode::Typstyle => {
509 FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
510 tab_spaces: formatter_indent_size,
511 max_width: formatter_print_width,
512 wrap_text: formatter_line_wrap,
513 ..typstyle_core::Config::default()
514 }))
515 }
516 FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
517 max_line_length: formatter_print_width,
518 indent_space: formatter_indent_size,
519 line_wrap: formatter_line_wrap,
520 ..typstfmt::Config::default()
521 })),
522 FormatterMode::Disable => FormatterConfig::Disable,
523 },
524 position_encoding: self.const_config.position_encoding,
525 }
526 }
527
528 #[cfg(feature = "preview")]
530 pub fn preview(&self) -> PreviewConfig {
531 PreviewConfig {
532 enable_partial_rendering: self.preview.partial_rendering,
533 refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
534 invert_colors: serde_json::to_string(&self.preview.invert_colors)
535 .unwrap_or_else(|_| "never".to_string()),
536 }
537 }
538
539 pub(crate) fn export_task(&self) -> ExportTask {
541 ExportTask {
542 when: self.export_pdf.clone(),
543 output: Some(self.output_path.clone()),
544 transform: vec![],
545 }
546 }
547
548 #[cfg(feature = "export")]
550 pub(crate) fn export(&self) -> ExportUserConfig {
551 let export = self.export_task();
552 ExportUserConfig {
553 export_target: self.export_target,
554 task: ProjectTask::ExportPdf(ExportPdfTask {
564 export,
565 pages: None, pdf_standards: self.pdf_standards().unwrap_or_default(),
567 no_pdf_tags: self.no_pdf_tags(),
568 creation_timestamp: self.creation_timestamp(),
569 }),
570 count_words: self.notify_status,
571 development: self.development,
572 }
573 }
574
575 pub fn font_opts(&self) -> CompileFontArgs {
577 let mut opts = self.font_opts.clone();
578
579 if let Some(system_fonts) = self.system_fonts.or_else(|| {
580 self.typst_extra_args
581 .as_ref()
582 .map(|x| !x.font.ignore_system_fonts)
583 }) {
584 opts.ignore_system_fonts = !system_fonts;
585 }
586
587 let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
588 let font_paths =
589 font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
590 if let Some(paths) = font_paths {
591 opts.font_paths.clone_from(paths);
592 }
593
594 let root = OnceLock::new();
595 for path in opts.font_paths.iter_mut() {
596 if path.is_relative() {
597 if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
598 let p = std::mem::take(path);
599 *path = root.join(p);
600 }
601 }
602 }
603
604 opts
605 }
606
607 pub fn package_opts(&self) -> CompilePackageArgs {
609 if let Some(extras) = &self.typst_extra_args {
610 return extras.package.clone();
611 }
612 CompilePackageArgs::default()
613 }
614
615 pub fn fonts(&self) -> Arc<FontResolverImpl> {
617 let font = || {
619 let opts = self.font_opts();
620
621 log::info!("creating SharedFontResolver with {opts:?}");
622 Derived(
623 crate::project::LspUniverseBuilder::resolve_fonts(opts)
624 .map(Arc::new)
625 .expect("failed to create font book"),
626 )
627 };
628 self.fonts.get_or_init(font).clone().0
629 }
630
631 pub fn inputs(&self) -> ImmutDict {
633 #[comemo::memoize]
634 fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
635 let mut dict = (**lhs).clone();
636 for (k, v) in rhs.iter() {
637 dict.insert(k.clone(), v.clone());
638 }
639
640 Arc::new(LazyHash::new(dict))
641 }
642
643 combine(self.user_inputs(), self.lsp_inputs.clone())
644 }
645
646 fn user_inputs(&self) -> ImmutDict {
647 static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
648
649 if let Some(extras) = &self.typst_extra_args {
650 return extras.inputs.clone();
651 }
652
653 EMPTY.clone()
654 }
655
656 pub fn typst_features(&self) -> Option<Features> {
658 let features = &self.typst_extra_args.as_ref()?.features;
659 Some(Features::from_iter(features.iter().map(|f| (*f).into())))
660 }
661
662 pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
664 Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
665 }
666
667 pub fn no_pdf_tags(&self) -> bool {
669 self.typst_extra_args
670 .as_ref()
671 .is_some_and(|x| x.no_pdf_tags)
672 }
673
674 pub fn ppi(&self) -> Option<f32> {
676 Some(self.typst_extra_args.as_ref()?.ppi)
677 }
678
679 pub fn creation_timestamp(&self) -> Option<i64> {
681 self.typst_extra_args.as_ref()?.creation_timestamp
682 }
683
684 pub fn certification_path(&self) -> Option<ImmutPath> {
686 self.typst_extra_args.as_ref()?.cert.clone()
687 }
688
689 #[allow(clippy::type_complexity)]
691 pub fn primary_opts(
692 &self,
693 ) -> (
694 Option<bool>,
695 &Vec<PathBuf>,
696 Option<&CompileFontArgs>,
697 Option<i64>,
698 Option<Arc<Path>>,
699 ) {
700 (
701 self.system_fonts,
702 &self.font_paths,
703 self.typst_extra_args.as_ref().map(|e| &e.font),
704 self.creation_timestamp(),
705 self.entry_resolver
706 .root(self.entry_resolver.resolve_default().as_ref()),
707 )
708 }
709
710 #[cfg(not(feature = "system"))]
711 fn create_physical_access_model(
712 &self,
713 client: &TypedLspClient<ServerState>,
714 ) -> Arc<dyn LspAccessModel> {
715 self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
716 }
717
718 #[cfg(feature = "system")]
719 fn create_physical_access_model(
720 &self,
721 _client: &TypedLspClient<ServerState>,
722 ) -> Arc<dyn LspAccessModel> {
723 use reflexo_typst::vfs::system::SystemAccessModel;
724 Arc::new(SystemAccessModel {})
725 }
726
727 pub(crate) fn watch_access_model(
728 &self,
729 client: &TypedLspClient<ServerState>,
730 ) -> &Arc<WatchAccessModel> {
731 let client = client.clone();
732 &self
733 .watch_access_model
734 .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
735 .0
736 }
737
738 pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
739 let access_model = || {
740 log::info!(
741 "creating AccessModel with delegation={:?}",
742 self.delegate_fs_requests
743 );
744 if self.delegate_fs_requests {
745 Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
746 } else {
747 Derived(self.create_physical_access_model(client))
748 }
749 };
750 DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
751 }
752}
753
754#[derive(Debug, Clone)]
757pub struct ConstConfig {
758 pub position_encoding: PositionEncoding,
761 pub cfg_change_registration: bool,
763 pub notify_will_rename_files: bool,
765 pub tokens_dynamic_registration: bool,
767 pub tokens_overlapping_token_support: bool,
769 pub tokens_multiline_token_support: bool,
771 pub doc_line_folding_only: bool,
773 pub doc_fmt_dynamic_registration: bool,
775 pub locale: Option<String>,
777}
778
779impl Default for ConstConfig {
780 fn default() -> Self {
781 Self::from(&InitializeParams::default())
782 }
783}
784
785impl From<&InitializeParams> for ConstConfig {
786 fn from(params: &InitializeParams) -> Self {
787 let position_encoding = {
792 PositionEncoding::Utf16
803 };
804
805 let workspace = params.capabilities.workspace.as_ref();
806 let file_operations = try_(|| workspace?.file_operations.as_ref());
807 let doc = params.capabilities.text_document.as_ref();
808 let sema = try_(|| doc?.semantic_tokens.as_ref());
809 let fold = try_(|| doc?.folding_range.as_ref());
810 let format = try_(|| doc?.formatting.as_ref());
811
812 let locale = params
813 .initialization_options
814 .as_ref()
815 .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
816 .or(params.locale.as_deref());
817
818 Self {
819 position_encoding,
820 cfg_change_registration: try_or(|| workspace?.configuration, false),
821 notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
822 tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
823 tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
824 tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
825 doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
826 doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
827 locale: locale.map(ToOwned::to_owned),
828 }
829 }
830}
831
832impl From<&dapts::InitializeRequestArguments> for ConstConfig {
833 fn from(params: &dapts::InitializeRequestArguments) -> Self {
834 let locale = params.locale.as_deref();
835
836 Self {
837 locale: locale.map(ToOwned::to_owned),
838 ..Default::default()
839 }
840 }
841}
842
843pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
846
847#[derive(Debug, Clone)]
850pub struct ConstDapConfig {
851 pub path_format: DapPathFormat,
853 pub lines_start_at1: bool,
855 pub columns_start_at1: bool,
857}
858
859impl Default for ConstDapConfig {
860 fn default() -> Self {
861 Self::from(&dapts::InitializeRequestArguments::default())
862 }
863}
864
865impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
866 fn from(params: &dapts::InitializeRequestArguments) -> Self {
867 Self {
868 path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
869 lines_start_at1: params.lines_start_at1.unwrap_or(true),
870 columns_start_at1: params.columns_start_at1.unwrap_or(true),
871 }
872 }
873}
874
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
877#[serde(rename_all = "camelCase")]
878pub enum FormatterMode {
879 #[default]
881 Disable,
882 Typstyle,
884 Typstfmt,
886}
887
888#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub enum SemanticTokensMode {
892 Disable,
894 #[default]
896 Enable,
897}
898
899#[derive(Debug, Default, Clone, Deserialize)]
901#[serde(rename_all = "camelCase")]
902pub struct PreviewFeat {
903 #[serde(default, deserialize_with = "deserialize_null_default")]
905 pub browsing: BrowsingPreviewOpts,
906 #[serde(default, deserialize_with = "deserialize_null_default")]
908 pub background: BackgroundPreviewOpts,
909 #[serde(default)]
911 pub refresh: Option<TaskWhen>,
912 #[serde(default, deserialize_with = "deserialize_null_default")]
914 pub partial_rendering: bool,
915 #[cfg(feature = "preview")]
917 #[serde(default, deserialize_with = "deserialize_null_default")]
918 pub invert_colors: PreviewInvertColors,
919}
920
921#[derive(Debug, Default, Clone, Deserialize)]
923pub struct LintFeat {
924 pub enabled: Option<bool>,
926 pub when: Option<TaskWhen>,
928}
929
930impl LintFeat {
931 pub fn when(&self) -> &TaskWhen {
933 if matches!(self.enabled, Some(false) | None) {
934 return &TaskWhen::Never;
935 }
936
937 self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
938 }
939}
940#[derive(Debug, Default, Clone, Deserialize)]
942#[serde(rename_all = "camelCase")]
943pub struct OnEnterFeat {
944 #[serde(default, deserialize_with = "deserialize_null_default")]
946 pub handle_list: bool,
947}
948
949#[derive(Debug, Default, Clone, Deserialize)]
951#[serde(rename_all = "camelCase")]
952pub struct BrowsingPreviewOpts {
953 pub args: Option<Vec<String>>,
955}
956
957#[derive(Debug, Default, Clone, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct BackgroundPreviewOpts {
961 #[serde(default, deserialize_with = "deserialize_null_default")]
963 pub enabled: bool,
964 pub args: Option<Vec<String>>,
966}
967
968#[derive(Debug, Clone, PartialEq, Default)]
972pub struct TypstExtraArgs {
973 pub root_dir: Option<ImmutPath>,
975 pub entry: Option<ImmutPath>,
977 pub inputs: ImmutDict,
979 pub font: CompileFontArgs,
981 pub package: CompilePackageArgs,
983 pub features: Vec<Feature>,
986 pub pdf_standard: Vec<PdfStandard>,
989 pub ppi: f32,
991 pub no_pdf_tags: bool,
996 pub creation_timestamp: Option<i64>,
998 pub cert: Option<ImmutPath>,
1000}
1001
1002pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
1003 SemanticTokensOptions {
1004 legend: SemanticTokensLegend {
1005 token_types: TokenType::iter()
1006 .filter(|e| *e != TokenType::None)
1007 .map(Into::into)
1008 .collect(),
1009 token_modifiers: Modifier::iter().map(Into::into).collect(),
1010 },
1011 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
1012 ..SemanticTokensOptions::default()
1013 }
1014}
1015
1016fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1017where
1018 T: Default + Deserialize<'de>,
1019 D: serde::Deserializer<'de>,
1020{
1021 let opt = Option::deserialize(deserializer)?;
1022 Ok(opt.unwrap_or_default())
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028 use serde_json::json;
1029 #[cfg(feature = "preview")]
1030 use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1031
1032 fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1033 temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1034 }
1035
1036 fn good_config(config: &mut Config, update: &JsonValue) {
1037 update_config(config, update).expect("not good");
1038 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1039 }
1040
1041 #[test]
1042 fn test_default_encoding() {
1043 let cc = ConstConfig::default();
1044 assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1045 }
1046
1047 #[test]
1048 fn test_config_update() {
1049 let mut config = Config::default();
1050
1051 let root_path = Path::new(if cfg!(windows) {
1052 "C:\\dummy-root"
1053 } else {
1054 "/dummy-root"
1055 });
1056
1057 let update = json!({
1058 "outputPath": "out",
1059 "exportPdf": "onSave",
1060 "rootPath": root_path,
1061 "semanticTokens": "enable",
1062 "formatterMode": "typstyle",
1063 "typstExtraArgs": ["--root", root_path]
1064 });
1065
1066 good_config(&mut config, &update);
1067
1068 let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1070 if has_source_date_epoch {
1071 let args = config.typst_extra_args.as_mut().unwrap();
1072 assert!(args.creation_timestamp.is_some());
1073 args.creation_timestamp = None;
1074 }
1075
1076 assert_eq!(config.output_path, PathPattern::new("out"));
1077 assert_eq!(config.export_pdf, TaskWhen::OnSave);
1078 assert_eq!(
1079 config.entry_resolver.root_path,
1080 Some(ImmutPath::from(root_path))
1081 );
1082 assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1083 assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1084 assert_eq!(
1085 config.typst_extra_args,
1086 Some(TypstExtraArgs {
1087 root_dir: Some(ImmutPath::from(root_path)),
1088 ppi: 144.0,
1089 ..TypstExtraArgs::default()
1090 })
1091 );
1092 }
1093
1094 #[test]
1095 fn test_namespaced_config() {
1096 let mut config = Config::default();
1097
1098 let update = json!({
1100 "exportPdf": "onSave",
1101 "tinymist": {
1102 "exportPdf": "onType",
1103 }
1104 });
1105
1106 good_config(&mut config, &update);
1107
1108 assert_eq!(config.export_pdf, TaskWhen::OnType);
1109 }
1110
1111 #[test]
1112 fn test_compile_status() {
1113 let mut config = Config::default();
1114
1115 let update = json!({
1116 "compileStatus": "enable",
1117 });
1118 good_config(&mut config, &update);
1119 assert!(config.notify_status);
1120
1121 let update = json!({
1122 "compileStatus": "disable",
1123 });
1124 good_config(&mut config, &update);
1125 assert!(!config.notify_status);
1126 }
1127
1128 #[test]
1129 fn test_config_creation_timestamp() {
1130 type Timestamp = Option<i64>;
1131
1132 fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1133 let mut config = Config::default();
1134
1135 f(&mut config);
1136
1137 let args = config.typst_extra_args;
1138 args.and_then(|args| args.creation_timestamp)
1139 }
1140
1141 let args_timestamp = timestamp(|config| {
1149 let update = json!({
1150 "typstExtraArgs": ["--creation-timestamp", "1234"]
1151 });
1152 good_config(config, &update);
1153 });
1154 assert!(args_timestamp.is_some());
1155
1156 }
1164
1165 #[test]
1166 fn test_empty_extra_args() {
1167 let mut config = Config::default();
1168 let update = json!({
1169 "typstExtraArgs": []
1170 });
1171
1172 good_config(&mut config, &update);
1173 }
1174
1175 #[test]
1176 fn test_null_args() {
1177 fn test_good_config(path: &str) -> Config {
1178 let mut obj = json!(null);
1179 let path = path.split('.').collect::<Vec<_>>();
1180 for p in path.iter().rev() {
1181 obj = json!({ *p: obj });
1182 }
1183
1184 let mut c = Config::default();
1185 good_config(&mut c, &obj);
1186 c
1187 }
1188
1189 test_good_config("root");
1190 test_good_config("rootPath");
1191 test_good_config("colorTheme");
1192 test_good_config("lint");
1193 test_good_config("customizedShowDocument");
1194 test_good_config("projectResolution");
1195 test_good_config("exportPdf");
1196 test_good_config("exportTarget");
1197 test_good_config("fontPaths");
1198 test_good_config("formatterMode");
1199 test_good_config("formatterPrintWidth");
1200 test_good_config("formatterIndentSize");
1201 test_good_config("formatterProseWrap");
1202 test_good_config("outputPath");
1203 test_good_config("semanticTokens");
1204 test_good_config("delegateFsRequests");
1205 test_good_config("supportHtmlInMarkdown");
1206 test_good_config("supportExtendedCodeAction");
1207 test_good_config("development");
1208 test_good_config("systemFonts");
1209
1210 test_good_config("completion");
1211 test_good_config("completion.triggerSuggest");
1212 test_good_config("completion.triggerParameterHints");
1213 test_good_config("completion.triggerSuggestAndParameterHints");
1214 test_good_config("completion.triggerOnSnippetPlaceholders");
1215 test_good_config("completion.symbol");
1216 test_good_config("completion.postfix");
1217 test_good_config("completion.postfixUfcs");
1218 test_good_config("completion.postfixUfcsLeft");
1219 test_good_config("completion.postfixUfcsRight");
1220 test_good_config("completion.postfixSnippets");
1221
1222 test_good_config("lint");
1223 test_good_config("lint.enabled");
1224 test_good_config("lint.when");
1225
1226 test_good_config("preview");
1227 test_good_config("preview.browsing");
1228 test_good_config("preview.browsing.args");
1229 test_good_config("preview.background");
1230 test_good_config("preview.background.enabled");
1231 test_good_config("preview.background.args");
1232 test_good_config("preview.refresh");
1233 test_good_config("preview.partialRendering");
1234 #[cfg(feature = "preview")]
1235 let c = test_good_config("preview.invertColors");
1236 #[cfg(feature = "preview")]
1237 assert_eq!(
1238 c.preview.invert_colors,
1239 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1240 );
1241 }
1242
1243 #[test]
1244 fn test_font_opts() {
1245 fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1246 let mut config = Config::default();
1247 if let Some(update) = update {
1248 good_config(&mut config, update);
1249 }
1250
1251 config.font_opts()
1252 }
1253
1254 let font_opts = opts(None);
1255 assert!(!font_opts.ignore_system_fonts);
1256
1257 let font_opts = opts(Some(&json!({})));
1258 assert!(!font_opts.ignore_system_fonts);
1259
1260 let font_opts = opts(Some(&json!({
1261 "typstExtraArgs": []
1262 })));
1263 assert!(!font_opts.ignore_system_fonts);
1264
1265 let font_opts = opts(Some(&json!({
1266 "systemFonts": false,
1267 })));
1268 assert!(font_opts.ignore_system_fonts);
1269
1270 let font_opts = opts(Some(&json!({
1271 "typstExtraArgs": ["--ignore-system-fonts"]
1272 })));
1273 assert!(font_opts.ignore_system_fonts);
1274
1275 let font_opts = opts(Some(&json!({
1276 "systemFonts": true,
1277 "typstExtraArgs": ["--ignore-system-fonts"]
1278 })));
1279 assert!(!font_opts.ignore_system_fonts);
1280 }
1281
1282 #[test]
1283 fn test_preview_opts() {
1284 fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1285 let mut config = Config::default();
1286 if let Some(update) = update {
1287 good_config(&mut config, update);
1288 }
1289
1290 config.preview
1291 }
1292
1293 let preview = opts(Some(&json!({
1294 "preview": {
1295 }
1296 })));
1297 assert_eq!(preview.refresh, None);
1298
1299 let preview = opts(Some(&json!({
1300 "preview": {
1301 "refresh":"onType"
1302 }
1303 })));
1304 assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1305
1306 let preview = opts(Some(&json!({
1307 "preview": {
1308 "refresh":"onSave"
1309 }
1310 })));
1311 assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1312 }
1313
1314 #[test]
1315 fn test_reject_abnormal_root() {
1316 let mut config = Config::default();
1317 let update = json!({
1318 "rootPath": ".",
1319 });
1320
1321 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1322 assert!(err.contains("absolute path"), "unexpected error: {err}");
1323 }
1324
1325 #[test]
1326 fn test_reject_abnormal_root2() {
1327 let mut config = Config::default();
1328 let update = json!({
1329 "typstExtraArgs": ["--root", "."]
1330 });
1331
1332 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1333 assert!(err.contains("absolute path"), "unexpected error: {err}");
1334 }
1335
1336 #[test]
1337 fn test_entry_by_extra_args() {
1338 let simple_config = {
1339 let mut config = Config::default();
1340 let update = json!({
1341 "typstExtraArgs": ["main.typ"]
1342 });
1343
1344 update_config(&mut config, &update).expect("updated");
1346 update_config(&mut config, &update).expect("updated");
1348 config
1349 };
1350 {
1351 let mut config = Config::default();
1352 let update = json!({
1353 "typstExtraArgs": ["main.typ", "main.typ"]
1354 });
1355 update_config(&mut config, &update).unwrap();
1356 let warns = format!("{:?}", config.warnings);
1357 assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1358 assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1359 }
1360 {
1361 let mut config = Config::default();
1362 let update = json!({
1363 "typstExtraArgs": ["main2.typ"],
1364 "tinymist": {
1365 "typstExtraArgs": ["main.typ"]
1366 }
1367 });
1368
1369 update_config(&mut config, &update).expect("updated");
1371 update_config(&mut config, &update).expect("updated");
1373
1374 assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1375 }
1376 }
1377
1378 #[test]
1379 fn test_default_formatting_config() {
1380 let config = Config::default().formatter();
1381 assert!(matches!(config.config, FormatterConfig::Disable));
1382 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1383 }
1384
1385 #[test]
1386 fn test_typstyle_formatting_config() {
1387 let config = Config {
1388 formatter_mode: FormatterMode::Typstyle,
1389 ..Config::default()
1390 };
1391 let config = config.formatter();
1392 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1393
1394 let typstyle_config = match config.config {
1395 FormatterConfig::Typstyle(e) => e,
1396 _ => panic!("unexpected configuration of formatter"),
1397 };
1398
1399 assert_eq!(typstyle_config.max_width, 120);
1400 }
1401
1402 #[test]
1403 fn test_typstyle_formatting_config_set_width() {
1404 let config = Config {
1405 formatter_mode: FormatterMode::Typstyle,
1406 formatter_print_width: Some(240),
1407 ..Config::default()
1408 };
1409 let config = config.formatter();
1410 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1411
1412 let typstyle_config = match config.config {
1413 FormatterConfig::Typstyle(e) => e,
1414 _ => panic!("unexpected configuration of formatter"),
1415 };
1416
1417 assert_eq!(typstyle_config.max_width, 240);
1418 }
1419
1420 #[test]
1421 fn test_typstyle_formatting_config_set_tab_spaces() {
1422 let config = Config {
1423 formatter_mode: FormatterMode::Typstyle,
1424 formatter_indent_size: Some(8),
1425 ..Config::default()
1426 };
1427 let config = config.formatter();
1428 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1429
1430 let typstyle_config = match config.config {
1431 FormatterConfig::Typstyle(e) => e,
1432 _ => panic!("unexpected configuration of formatter"),
1433 };
1434
1435 assert_eq!(typstyle_config.tab_spaces, 8);
1436 }
1437
1438 #[test]
1439 #[cfg(feature = "preview")]
1440 fn test_default_preview_config() {
1441 let config = Config::default().preview();
1442 assert!(!config.enable_partial_rendering);
1443 assert_eq!(config.refresh_style, TaskWhen::OnType);
1444 assert_eq!(config.invert_colors, "\"never\"");
1445 }
1446
1447 #[test]
1448 #[cfg(feature = "preview")]
1449 fn test_preview_config() {
1450 let config = Config {
1451 preview: PreviewFeat {
1452 partial_rendering: true,
1453 refresh: Some(TaskWhen::OnSave),
1454 invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1455 ..PreviewFeat::default()
1456 },
1457 ..Config::default()
1458 }
1459 .preview();
1460
1461 assert!(config.enable_partial_rendering);
1462 assert_eq!(config.refresh_style, TaskWhen::OnSave);
1463 assert_eq!(config.invert_colors, "\"auto\"");
1464 }
1465
1466 #[test]
1467 fn test_default_lsp_config_initialize() {
1468 let (_conf, err) =
1469 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1470 assert!(err.is_none());
1471 }
1472
1473 #[test]
1474 fn test_default_dap_config_initialize() {
1475 let (_conf, err) = Config::extract_dap_params(
1476 dapts::InitializeRequestArguments::default(),
1477 CompileFontArgs::default(),
1478 );
1479 assert!(err.is_none());
1480 }
1481
1482 #[test]
1483 fn test_config_package_path_from_env() {
1484 let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
1485
1486 temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
1487 let (conf, err) =
1488 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1489 assert!(err.is_none());
1490 let applied_cache_path = conf
1491 .typst_extra_args
1492 .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
1493 assert!(applied_cache_path);
1494 });
1495 }
1496
1497 #[test]
1498 #[cfg(feature = "preview")]
1499 fn test_invert_colors_validation() {
1500 fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
1501 Ok(serde_json::from_str(s)?)
1502 }
1503
1504 assert_eq!(
1505 test(r#""never""#).unwrap(),
1506 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1507 );
1508 assert_eq!(
1509 test(r#""auto""#).unwrap(),
1510 PreviewInvertColors::Enum(PreviewInvertColor::Auto)
1511 );
1512 assert_eq!(
1513 test(r#""always""#).unwrap(),
1514 PreviewInvertColors::Enum(PreviewInvertColor::Always)
1515 );
1516 assert!(test(r#""e""#).is_err());
1517
1518 assert_eq!(
1519 test(r#"{"rest": "never"}"#).unwrap(),
1520 PreviewInvertColors::Object(PreviewInvertColorObject {
1521 image: PreviewInvertColor::Never,
1522 rest: PreviewInvertColor::Never,
1523 })
1524 );
1525 assert_eq!(
1526 test(r#"{"image": "always"}"#).unwrap(),
1527 PreviewInvertColors::Object(PreviewInvertColorObject {
1528 image: PreviewInvertColor::Always,
1529 rest: PreviewInvertColor::Never,
1530 })
1531 );
1532 assert_eq!(
1533 test(r#"{}"#).unwrap(),
1534 PreviewInvertColors::Object(PreviewInvertColorObject {
1535 image: PreviewInvertColor::Never,
1536 rest: PreviewInvertColor::Never,
1537 })
1538 );
1539 assert_eq!(
1540 test(r#"{"unknown": "ovo"}"#).unwrap(),
1541 PreviewInvertColors::Object(PreviewInvertColorObject {
1542 image: PreviewInvertColor::Never,
1543 rest: PreviewInvertColor::Never,
1544 })
1545 );
1546 assert!(test(r#"{"image": "e"}"#).is_err());
1547 }
1548}