1use core::fmt;
2use std::borrow::Cow;
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, LazyLock, OnceLock};
5
6use clap::{
7 error::{ContextKind, ContextValue, ErrorKind},
8 Parser,
9};
10use itertools::Itertools;
11use lsp_types::*;
12use reflexo::error::IgnoreLogging;
13use reflexo::CowStr;
14use reflexo_typst::{ImmutPath, TypstDict};
15use serde::{Deserialize, Serialize};
16use serde_json::{Map, Value as JsonValue};
17use strum::IntoEnumIterator;
18use task::{FormatUserConfig, FormatterConfig};
19use tinymist_l10n::DebugL10n;
20use tinymist_project::{DynAccessModel, LspAccessModel};
21use tinymist_query::analysis::{Modifier, TokenType};
22use tinymist_query::{url_to_path, CompletionFeat, PositionEncoding};
23use tinymist_render::PeriscopeArgs;
24use tinymist_std::error::prelude::*;
25use tinymist_task::ExportTarget;
26use typst::foundations::IntoValue;
27use typst::Features;
28use typst_shim::utils::LazyHash;
29use typst_shim::SYNTAX_ONLY;
30
31use super::*;
32use crate::input::WatchAccessModel;
33use crate::project::{
34 EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen,
35};
36use crate::world::font::FontResolverImpl;
37
38#[cfg(feature = "export")]
39use task::ExportUserConfig;
40#[cfg(feature = "preview")]
41use tinymist_preview::{PreviewConfig, PreviewInvertColors};
42
43#[cfg(feature = "export")]
44use crate::project::{ExportPdfTask, ProjectTask};
45
46const CONFIG_ITEMS: &[&str] = &[
48 "tinymist",
49 "colorTheme",
50 "compileStatus",
51 "lint",
52 "completion",
53 "customizedShowDocument",
54 "development",
55 "delegateFsRequests",
56 "exportPdf",
57 "exportTarget",
58 "fontPaths",
59 "formatterMode",
60 "formatterPrintWidth",
61 "formatterIndentSize",
62 "formatterProseWrap",
63 "hoverPeriscope",
64 "onEnter",
65 "outputPath",
66 "syntaxOnly",
67 "preview",
68 "projectResolution",
69 "rootPath",
70 "semanticTokens",
71 "supportClientCodelens",
72 "supportExtendedCodeAction",
73 "supportHtmlInMarkdown",
74 "systemFonts",
75 "triggerParameterHints",
76 "triggerSuggest",
77 "triggerSuggestAndParameterHints",
78 "typstExtraArgs",
79];
80#[derive(Debug, Default, Clone)]
88pub struct Config {
89 pub const_config: ConstConfig,
91 pub const_dap_config: ConstDapConfig,
93
94 pub delegate_fs_requests: bool,
96 pub customized_show_document: bool,
98 pub has_default_entry_path: bool,
100 pub notify_status: bool,
102 pub support_html_in_markdown: bool,
104 pub support_client_codelens: bool,
109 pub extended_code_action: bool,
112 pub development: bool,
114 pub syntax_only: bool,
116
117 pub color_theme: Option<String>,
119 pub entry_resolver: EntryResolver,
121 pub lsp_inputs: ImmutDict,
123 pub periscope_args: Option<PeriscopeArgs>,
125 pub typst_extra_args: Option<TypstExtraArgs>,
127 pub semantic_tokens: SemanticTokensMode,
129
130 pub completion: CompletionFeat,
132 pub preview: PreviewFeat,
134 pub lint: LintFeat,
136 pub on_enter: OnEnterFeat,
138
139 pub font_opts: CompileFontArgs,
141 pub font_paths: Vec<PathBuf>,
143 pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
145 pub system_fonts: Option<bool>,
147
148 pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
150 pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
152
153 pub export_target: ExportTarget,
155 pub export_pdf: TaskWhen,
157 pub output_path: PathPattern,
159
160 pub formatter_mode: FormatterMode,
162 pub formatter_print_width: Option<u32>,
165 pub formatter_indent_size: Option<u32>,
167 pub formatter_prose_wrap: Option<FormatterProseWrap>,
169 pub warnings: Vec<CowStr>,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct RestartScopedClientOptions {
176 notify_status: bool,
177 trigger_suggest: bool,
178 trigger_parameter_hints: bool,
179 trigger_suggest_and_parameter_hints: bool,
180 support_html_in_markdown: bool,
181 support_client_codelens: bool,
182 extended_code_action: bool,
183 customized_show_document: bool,
184 delegate_fs_requests: bool,
185}
186
187impl Config {
188 pub fn new(
190 const_config: ConstConfig,
191 roots: Vec<ImmutPath>,
192 font_opts: CompileFontArgs,
193 ) -> Self {
194 let mut config = Self {
195 const_config,
196 const_dap_config: ConstDapConfig::default(),
197 entry_resolver: EntryResolver {
198 roots,
199 ..EntryResolver::default()
200 },
201 font_opts,
202 ..Self::default()
203 };
204 config
205 .update_by_map(&Map::default())
206 .log_error("failed to assign Config defaults");
207 config
208 }
209
210 fn configure_completion_access(&mut self) {
211 #[cfg(feature = "system")]
212 {
213 self.completion.path_completion_by_filesystem = !self.delegate_fs_requests;
214 log::info!(
215 "completion.path.config: delegate_fs_requests={}, path_completion_by_filesystem={}",
216 self.delegate_fs_requests,
217 self.completion.path_completion_by_filesystem
218 );
219 }
220 }
221
222 pub fn extract_lsp_params(
228 params: InitializeParams,
229 font_args: CompileFontArgs,
230 ) -> (Self, Option<ResponseError>) {
231 let roots = match params.workspace_folders.as_ref() {
233 Some(roots) => roots
234 .iter()
235 .map(|root| ImmutPath::from(url_to_path(&root.uri)))
236 .collect(),
237 #[allow(deprecated)] None => params
239 .root_uri
240 .as_ref()
241 .map(|uri| ImmutPath::from(url_to_path(uri)))
242 .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into()))
243 .into_iter()
244 .collect(),
245 };
246 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
247
248 if let Some(locale) = config.const_config.locale.as_ref() {
250 tinymist_l10n::set_locale(locale);
251 }
252 config.configure_syntax_only();
253
254 let err = params
255 .initialization_options
256 .and_then(|init| config.update(&init).map_err(invalid_params).err());
257
258 (config, err)
259 }
260
261 pub fn extract_dap_params(
267 params: dapts::InitializeRequestArguments,
268 font_args: CompileFontArgs,
269 ) -> (Self, Option<ResponseError>) {
270 let cwd = std::env::current_dir()
272 .expect("failed to get current directory")
273 .into();
274
275 let roots = vec![cwd];
277 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
278 config.const_dap_config = ConstDapConfig::from(¶ms);
279
280 if let Some(locale) = config.const_config.locale.as_ref() {
282 tinymist_l10n::set_locale(locale);
283 }
284
285 (config, None)
286 }
287
288 pub fn get_items() -> Vec<ConfigurationItem> {
291 CONFIG_ITEMS
292 .iter()
293 .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
294 .map(|section| ConfigurationItem {
295 section: Some(section),
296 ..ConfigurationItem::default()
297 })
298 .collect()
299 }
300
301 pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
315 let mut namespace = Map::new();
317 let mut map = Map::new();
318
319 for (&item, (namespaced, top_level)) in CONFIG_ITEMS.iter().zip(values.into_iter().tuples())
320 {
321 if item == "tinymist" {
322 namespace = top_level.as_object().cloned().unwrap_or_default();
323 continue;
324 }
325
326 map.insert(
327 item.to_owned(),
328 if !namespaced.is_null() {
329 namespaced
330 } else {
331 top_level
332 },
333 );
334 }
335
336 for (key, value) in namespace {
339 match map.get(&key) {
340 Some(existing) if !existing.is_null() => {}
341 _ => {
342 map.insert(key, value);
343 }
344 }
345 }
346
347 map
348 }
349
350 pub fn update(&mut self, update: &JsonValue) -> Result<()> {
355 if let JsonValue::Object(update) = update {
356 self.update_by_map(update)
357 } else {
358 tinymist_l10n::bail!(
359 "tinymist.config.invalidObject",
360 "invalid configuration object: {object}",
361 object = update.debug_l10n(),
362 )
363 }
364 }
365
366 fn unpack_namespace(update: &Map<String, JsonValue>) -> Cow<'_, Map<String, JsonValue>> {
372 let Some(JsonValue::Object(namespaced)) = update.get("tinymist") else {
373 return Cow::Borrowed(update);
374 };
375
376 let mut flat = update.clone();
378 flat.extend(namespaced.clone());
379
380 Cow::Owned(flat)
381 }
382
383 pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> Result<()> {
388 log::info!(
389 "ServerState: config update_by_map {}",
390 serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
391 );
392
393 let flattened = Self::unpack_namespace(update);
394 let update = flattened.as_ref();
395
396 self.warnings.clear();
397
398 macro_rules! try_deserialize {
399 ($ty:ty, $key:expr) => {
400 update.get($key).and_then(|v| {
401 <$ty>::deserialize(v)
402 .inspect_err(|err| {
403 if v.is_null() {
406 return;
407 }
408
409 self.warnings.push(tinymist_l10n::t!(
410 "tinymist.config.deserializeError",
411 "failed to deserialize \"{key}\": {err}",
412 key = $key.debug_l10n(),
413 err = err.debug_l10n(),
414 ));
415 })
416 .ok()
417 })
418 };
419 }
420
421 macro_rules! assign_config {
422 ($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
423 let v = try_deserialize!($ty, $bind);
424 self.$($field_path).+ = v.unwrap_or_default();
425 };
426 ($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
427 let v = try_deserialize!($ty, $bind);
428 self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
429 };
430 }
431
432 assign_config!(color_theme := "colorTheme"?: Option<String>);
433 assign_config!(lint := "lint"?: LintFeat);
434 assign_config!(completion := "completion"?: CompletionFeat);
435 assign_config!(on_enter := "onEnter"?: OnEnterFeat);
436 assign_config!(completion.trigger_suggest := "triggerSuggest"?: bool);
437 assign_config!(completion.trigger_parameter_hints := "triggerParameterHints"?: bool);
438 assign_config!(completion.trigger_suggest_and_parameter_hints := "triggerSuggestAndParameterHints"?: bool);
439 assign_config!(customized_show_document := "customizedShowDocument"?: bool);
440 assign_config!(entry_resolver.project_resolution := "projectResolution"?: ProjectResolutionKind);
441 assign_config!(export_pdf := "exportPdf"?: TaskWhen);
442 assign_config!(export_target := "exportTarget"?: ExportTarget);
443 assign_config!(font_paths := "fontPaths"?: Vec<_>);
444 assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
445 assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
446 assign_config!(formatter_indent_size := "formatterIndentSize"?: Option<u32>);
447 assign_config!(formatter_prose_wrap := "formatterProseWrap"?: Option<FormatterProseWrap>);
448 assign_config!(output_path := "outputPath"?: PathPattern);
449 assign_config!(preview := "preview"?: PreviewFeat);
450 assign_config!(lint := "lint"?: LintFeat);
451 assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
452 assign_config!(delegate_fs_requests := "delegateFsRequests"?: bool);
453 assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
454 assign_config!(support_client_codelens := "supportClientCodelens"?: bool);
455 assign_config!(extended_code_action := "supportExtendedCodeAction"?: bool);
456 assign_config!(development := "development"?: bool);
457 assign_config!(system_fonts := "systemFonts"?: Option<bool>);
458
459 self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
460 Some("enable") => true,
461 Some("disable") | None => false,
462 Some(value) => {
463 self.warnings.push(tinymist_l10n::t!(
464 "tinymist.config.badCompileStatus",
465 "compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
466 value = value.debug_l10n(),
467 ));
468
469 false
470 }
471 };
472 self.syntax_only = match try_(|| update.get("syntaxOnly")?.as_str()) {
473 #[cfg(feature = "battery")]
474 Some("onPowerSaving") => tinymist_std::battery::is_power_saving(),
475 #[cfg(not(feature = "battery"))]
476 Some("onPowerSaving") => {
477 log::warn!("battery feature is not enabled for checking power saving mode, syntax-only mode is disabled");
478 false
479 }
480 Some("enable") => true,
481 Some("disable" | "auto") | None => false,
482 Some(value) => {
483 self.warnings.push(tinymist_l10n::t!(
484 "tinymist.config.badSyntaxOnly",
485 "syntaxOnly must be either `\"enable\"`, `\"disable\", `\"onPowerSaving\"`, or `\"auto\"`, got {value}",
486 value = value.debug_l10n(),
487 ));
488
489 false
490 }
491 };
492
493 self.periscope_args = match update.get("hoverPeriscope") {
495 Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()),
496 Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None,
497 Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
498 Ok(args) => Some(args),
499 Err(err) => {
500 self.warnings.push(tinymist_l10n::t!(
501 "tinymist.config.badHoverPeriscope",
502 "failed to parse hoverPeriscope: {err}",
503 err = err.debug_l10n(),
504 ));
505 None
506 }
507 },
508 };
509 if let Some(args) = self.periscope_args.as_mut() {
510 if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") {
511 "always".clone_into(&mut args.invert_color);
512 }
513 }
514
515 {
516 let raw_args = update.get("typstExtraArgs");
517 let typst_args: Vec<String> = match raw_args.cloned().map(serde_json::from_value) {
518 Some(Ok(args)) => args,
519 Some(Err(err)) => {
520 self.warnings
521 .push(format_typst_extra_args_error(&raw_args, &err, None));
522 None
523 }
524 None => None,
527 }
528 .unwrap_or_default();
529 let empty_typst_args = typst_args.is_empty();
530
531 let args = match CompileOnceArgs::try_parse_from(
532 Some("typst-cli".to_owned())
533 .into_iter()
534 .chain(typst_args.iter().cloned()),
535 ) {
536 Ok(args) => args,
537 Err(err) => {
538 let hint = typst_extra_args_parse_hint(&typst_args, &err);
539 self.warnings
540 .push(format_typst_extra_args_error(&raw_args, &err, hint));
541
542 if empty_typst_args {
543 CompileOnceArgs::default()
544 } else {
545 CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
547 .inspect_err(|err| {
548 log::error!("failed to make default typstExtraArgs: {err}");
549 })
550 .unwrap_or_default()
551 }
552 }
553 };
554
555 self.typst_extra_args = Some(TypstExtraArgs {
557 inputs: args.resolve_inputs().unwrap_or_default(),
558 entry: args.input.map(|e| Path::new(&e).into()),
559 root_dir: args.root.as_ref().map(|r| r.as_path().into()),
560 font: args.font,
561 package: args.package,
562 pdf_standard: args.pdf.standard,
563 no_pdf_tags: args.pdf.no_tags,
564 ppi: args.png.ppi,
565 features: args.features,
566 creation_timestamp: args.creation_timestamp,
567 cert: args.cert.as_deref().map(From::from),
568 });
569 }
570
571 self.entry_resolver.root_path =
572 try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
573 self.typst_extra_args
574 .as_ref()
575 .and_then(|e| e.root_dir.clone())
576 });
577 self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
578 self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
579 self.lsp_inputs = {
580 let mut dict = TypstDict::default();
581
582 #[derive(Serialize)]
583 #[serde(rename_all = "camelCase")]
584 struct PreviewInputs {
585 pub version: u32,
586 pub theme: String,
587 }
588
589 dict.insert(
590 "x-preview".into(),
591 serde_json::to_string(&PreviewInputs {
592 version: 1,
593 theme: self.color_theme.clone().unwrap_or_default(),
594 })
595 .unwrap()
596 .into_value(),
597 );
598
599 Arc::new(LazyHash::new(dict))
600 };
601
602 self.configure_completion_access();
603 self.validate()
604 }
605
606 pub fn validate(&self) -> Result<()> {
608 self.entry_resolver.validate()?;
609
610 Ok(())
611 }
612
613 pub fn configure_syntax_only(&self) {
615 if self.syntax_only {
616 log::info!("Server: running lsp in syntax-only mode, some features may be disabled");
617 SYNTAX_ONLY.store(true, std::sync::atomic::Ordering::SeqCst);
618 } else {
619 log::info!("Server: running lsp in full mode");
620 SYNTAX_ONLY.store(false, std::sync::atomic::Ordering::SeqCst);
621 }
622 }
623
624 pub fn formatter(&self) -> FormatUserConfig {
626 let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
627 let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
628 let formatter_prose_wrap = self.formatter_prose_wrap.unwrap_or_default();
629 let formatter_line_wrap = formatter_prose_wrap != FormatterProseWrap::None;
632
633 FormatUserConfig {
634 config: match self.formatter_mode {
635 FormatterMode::Typstyle => {
636 FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
637 tab_spaces: formatter_indent_size,
638 max_width: formatter_print_width,
639 wrap_mode: formatter_prose_wrap.to_typstyle(),
640 ..typstyle_core::Config::default()
641 }))
642 }
643 FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
644 max_line_length: formatter_print_width,
645 indent_space: formatter_indent_size,
646 line_wrap: formatter_line_wrap,
647 ..typstfmt::Config::default()
648 })),
649 FormatterMode::Disable => FormatterConfig::Disable,
650 },
651 position_encoding: self.const_config.position_encoding,
652 }
653 }
654
655 #[cfg(feature = "preview")]
657 pub fn preview(&self) -> PreviewConfig {
658 PreviewConfig {
659 format: ExportTarget::Paged,
660 enable_partial_rendering: self.preview.partial_rendering,
661 refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
662 invert_colors: serde_json::to_string(&self.preview.invert_colors)
663 .unwrap_or_else(|_| "never".to_string()),
664 }
665 }
666
667 pub(crate) fn export_task(&self) -> ExportTask {
669 ExportTask {
670 when: self.export_pdf.clone(),
671 output: Some(self.output_path.clone()),
672 transform: vec![],
673 }
674 }
675
676 #[cfg(feature = "export")]
678 pub(crate) fn export(&self) -> ExportUserConfig {
679 let export = self.export_task();
680 ExportUserConfig {
681 export_target: self.export_target,
682 task: ProjectTask::ExportPdf(ExportPdfTask {
692 export,
693 pages: None, pdf_standards: self.pdf_standards().unwrap_or_default(),
695 no_pdf_tags: self.no_pdf_tags(),
696 creation_timestamp: self.creation_timestamp(),
697 }),
698 count_words: self.notify_status,
699 development: self.development,
700 }
701 }
702
703 pub fn font_opts(&self) -> CompileFontArgs {
705 let mut opts = self.font_opts.clone();
706
707 if let Some(system_fonts) = self.system_fonts.or_else(|| {
708 self.typst_extra_args
709 .as_ref()
710 .map(|x| !x.font.ignore_system_fonts)
711 }) {
712 opts.ignore_system_fonts = !system_fonts;
713 }
714
715 let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
716 let font_paths =
717 font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
718 if let Some(paths) = font_paths {
719 opts.font_paths.clone_from(paths);
720 }
721
722 let root = OnceLock::new();
723 for path in opts.font_paths.iter_mut() {
724 if path.is_relative() {
725 if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
726 let p = std::mem::take(path);
727 *path = root.join(p);
728 }
729 }
730 }
731
732 opts
733 }
734
735 pub fn package_opts(&self) -> CompilePackageArgs {
737 if let Some(extras) = &self.typst_extra_args {
738 return extras.package.clone();
739 }
740 CompilePackageArgs::default()
741 }
742
743 pub fn fonts(&self) -> Arc<FontResolverImpl> {
745 let font = || {
747 let opts = self.font_opts();
748
749 log::info!("creating SharedFontResolver with {opts:?}");
750 Derived(
751 crate::project::LspUniverseBuilder::resolve_fonts(opts)
752 .map(Arc::new)
753 .expect("failed to create font book"),
754 )
755 };
756 self.fonts.get_or_init(font).clone().0
757 }
758
759 pub fn inputs(&self) -> ImmutDict {
761 #[comemo::memoize]
762 fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
763 let mut dict = (**lhs).clone();
764 for (k, v) in rhs.iter() {
765 dict.insert(k.clone(), v.clone());
766 }
767
768 Arc::new(LazyHash::new(dict))
769 }
770
771 combine(self.user_inputs(), self.lsp_inputs.clone())
772 }
773
774 fn user_inputs(&self) -> ImmutDict {
775 static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
776
777 if let Some(extras) = &self.typst_extra_args {
778 return extras.inputs.clone();
779 }
780
781 EMPTY.clone()
782 }
783
784 pub fn typst_features(&self) -> Option<Features> {
786 let features = &self.typst_extra_args.as_ref()?.features;
787 Some(Features::from_iter(features.iter().map(|f| (*f).into())))
788 }
789
790 pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
792 Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
793 }
794
795 pub fn no_pdf_tags(&self) -> bool {
797 self.typst_extra_args
798 .as_ref()
799 .is_some_and(|x| x.no_pdf_tags)
800 }
801
802 pub fn ppi(&self) -> Option<f32> {
804 Some(self.typst_extra_args.as_ref()?.ppi)
805 }
806
807 pub fn creation_timestamp(&self) -> Option<i64> {
809 self.typst_extra_args.as_ref()?.creation_timestamp
810 }
811
812 pub fn certification_path(&self) -> Option<ImmutPath> {
814 self.typst_extra_args.as_ref()?.cert.clone()
815 }
816
817 #[allow(clippy::type_complexity)]
819 pub fn primary_opts(
820 &self,
821 ) -> (
822 bool,
823 ImmutDict,
824 ExportTarget,
825 Option<Vec<typst::Feature>>,
826 Option<ImmutPath>,
827 CompilePackageArgs,
828 Option<bool>,
829 CompileFontArgs,
830 Option<i64>,
831 Option<Arc<Path>>,
832 ) {
833 (
834 self.syntax_only,
836 self.user_inputs(),
838 self.export_target,
839 self.typst_features().map(|feat| {
840 let mut features = vec![];
841 if feat.is_enabled(typst::Feature::Html) {
842 features.push(typst::Feature::Html);
843 }
844 if feat.is_enabled(typst::Feature::A11yExtras) {
845 features.push(typst::Feature::A11yExtras);
846 }
847
848 features
849 }),
850 self.certification_path(),
852 self.package_opts(),
853 self.system_fonts,
855 self.font_opts(),
856 self.creation_timestamp(),
857 self.entry_resolver
859 .root(self.entry_resolver.resolve_default().as_ref()),
860 )
861 }
862
863 pub fn restart_scoped_client_opts(&self) -> RestartScopedClientOptions {
865 RestartScopedClientOptions {
866 notify_status: self.notify_status,
867 trigger_suggest: self.completion.trigger_suggest,
868 trigger_parameter_hints: self.completion.trigger_parameter_hints,
869 trigger_suggest_and_parameter_hints: self
870 .completion
871 .trigger_suggest_and_parameter_hints,
872 support_html_in_markdown: self.support_html_in_markdown,
873 support_client_codelens: self.support_client_codelens,
874 extended_code_action: self.extended_code_action,
875 customized_show_document: self.customized_show_document,
876 delegate_fs_requests: self.delegate_fs_requests,
877 }
878 }
879
880 #[cfg(not(feature = "system"))]
881 fn create_physical_access_model(
882 &self,
883 client: &TypedLspClient<ServerState>,
884 ) -> Arc<dyn LspAccessModel> {
885 self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
886 }
887
888 #[cfg(feature = "system")]
889 fn create_physical_access_model(
890 &self,
891 _client: &TypedLspClient<ServerState>,
892 ) -> Arc<dyn LspAccessModel> {
893 use reflexo_typst::vfs::system::SystemAccessModel;
894 Arc::new(SystemAccessModel {})
895 }
896
897 pub(crate) fn watch_access_model(
898 &self,
899 client: &TypedLspClient<ServerState>,
900 ) -> &Arc<WatchAccessModel> {
901 let client = client.clone();
902 &self
903 .watch_access_model
904 .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
905 .0
906 }
907
908 pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
909 let access_model = || {
910 log::info!(
911 "creating AccessModel with delegation={:?}",
912 self.delegate_fs_requests
913 );
914 if self.delegate_fs_requests {
915 Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
916 } else {
917 Derived(self.create_physical_access_model(client))
918 }
919 };
920 DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
921 }
922}
923
924fn typst_extra_args_parse_hint(args: &[String], err: &clap::Error) -> Option<CowStr> {
925 if err.kind() != ErrorKind::UnknownArgument {
926 return None;
927 }
928
929 let Some(ContextValue::String(invalid_arg)) = err.get(ContextKind::InvalidArg) else {
930 return None;
931 };
932 let Some(ContextValue::String(option)) = err.get(ContextKind::SuggestedArg) else {
933 return None;
934 };
935
936 let value = invalid_arg.strip_prefix(option)?;
937 if !value.chars().next().is_some_and(char::is_whitespace) {
938 return None;
939 }
940
941 let value = value.trim();
942 if value.is_empty() {
943 return None;
944 }
945
946 let (arg, value) = args
947 .iter()
948 .map(|arg| arg.trim())
949 .filter(|arg| arg.starts_with(invalid_arg.as_str()))
950 .find_map(|arg| {
951 let value = arg.strip_prefix(option.as_str())?;
952 value
953 .chars()
954 .next()
955 .is_some_and(char::is_whitespace)
956 .then_some((arg, value.trim()))
957 })
958 .filter(|(_, value)| !value.is_empty())
959 .unwrap_or((invalid_arg.as_str(), value));
960
961 Some(tinymist_l10n::t!(
962 "tinymist.config.badTypstExtraArgs.joinedOptionValueHint",
963 "`tinymist.typstExtraArgs` is an argv array, so `\"{arg}\"` is treated as one argument. Split the option and value into two entries like `\"{option}\", \"{value}\"`, or use `\"{option}={value}\"` when the option accepts the `--flag=value` form.",
964 arg = arg.into(),
965 option = option.as_str().into(),
966 value = value.into(),
967 ))
968}
969
970fn format_typst_extra_args_error(
971 args: &impl fmt::Debug,
972 err: &impl std::error::Error,
973 hint: Option<CowStr>,
974) -> CowStr {
975 log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
976 let message = tinymist_l10n::t!(
977 "tinymist.config.badTypstExtraArgs",
978 "failed to parse typstExtraArgs: {err}, args: {args}",
979 err = err.debug_l10n(),
980 args = args.debug_l10n(),
981 );
982
983 match hint {
984 Some(hint) => format!("{message}\n{hint}").into(),
985 None => message,
986 }
987}
988
989#[derive(Debug, Clone)]
992pub struct ConstConfig {
993 pub position_encoding: PositionEncoding,
996 pub cfg_change_registration: bool,
998 pub notify_will_rename_files: bool,
1000 pub tokens_dynamic_registration: bool,
1002 pub tokens_overlapping_token_support: bool,
1004 pub tokens_multiline_token_support: bool,
1006 pub doc_line_folding_only: bool,
1008 pub doc_fmt_dynamic_registration: bool,
1010 pub completion_insert_replace_support: bool,
1012 pub locale: Option<String>,
1014}
1015
1016impl Default for ConstConfig {
1017 fn default() -> Self {
1018 Self::from(&InitializeParams::default())
1019 }
1020}
1021
1022impl From<&InitializeParams> for ConstConfig {
1023 fn from(params: &InitializeParams) -> Self {
1024 let position_encoding = {
1029 PositionEncoding::Utf16
1040 };
1041
1042 let workspace = params.capabilities.workspace.as_ref();
1043 let file_operations = try_(|| workspace?.file_operations.as_ref());
1044 let doc = params.capabilities.text_document.as_ref();
1045 let sema = try_(|| doc?.semantic_tokens.as_ref());
1046 let fold = try_(|| doc?.folding_range.as_ref());
1047 let format = try_(|| doc?.formatting.as_ref());
1048 let completion_item = try_(|| doc?.completion.as_ref()?.completion_item.as_ref());
1049
1050 let locale = params
1051 .initialization_options
1052 .as_ref()
1053 .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
1054 .or(params.locale.as_deref());
1055
1056 Self {
1057 position_encoding,
1058 cfg_change_registration: try_or(|| workspace?.configuration, false),
1059 notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
1060 tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
1061 tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
1062 tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
1063 doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
1064 doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
1065 completion_insert_replace_support: try_or(
1066 || completion_item?.insert_replace_support,
1067 false,
1068 ),
1069 locale: locale.map(ToOwned::to_owned),
1070 }
1071 }
1072}
1073
1074impl From<&dapts::InitializeRequestArguments> for ConstConfig {
1075 fn from(params: &dapts::InitializeRequestArguments) -> Self {
1076 let locale = params.locale.as_deref();
1077
1078 Self {
1079 locale: locale.map(ToOwned::to_owned),
1080 ..Default::default()
1081 }
1082 }
1083}
1084
1085pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
1088
1089#[derive(Debug, Clone)]
1092pub struct ConstDapConfig {
1093 pub path_format: DapPathFormat,
1095 pub lines_start_at1: bool,
1097 pub columns_start_at1: bool,
1099}
1100
1101impl Default for ConstDapConfig {
1102 fn default() -> Self {
1103 Self::from(&dapts::InitializeRequestArguments::default())
1104 }
1105}
1106
1107impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
1108 fn from(params: &dapts::InitializeRequestArguments) -> Self {
1109 Self {
1110 path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
1111 lines_start_at1: params.lines_start_at1.unwrap_or(true),
1112 columns_start_at1: params.columns_start_at1.unwrap_or(true),
1113 }
1114 }
1115}
1116
1117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub enum FormatterMode {
1121 Disable,
1123 #[default]
1125 Typstyle,
1126 Typstfmt,
1128}
1129
1130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1136pub enum FormatterProseWrap {
1137 #[default]
1139 None,
1140 Fill,
1142 Sentence,
1144}
1145
1146impl FormatterProseWrap {
1147 pub fn to_typstyle(self) -> typstyle_core::WrapMode {
1149 match self {
1150 FormatterProseWrap::None => typstyle_core::WrapMode::None,
1151 FormatterProseWrap::Fill => typstyle_core::WrapMode::Fill,
1152 FormatterProseWrap::Sentence => typstyle_core::WrapMode::Sentence,
1153 }
1154 }
1155}
1156
1157impl Serialize for FormatterProseWrap {
1158 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1159 where
1160 S: serde::Serializer,
1161 {
1162 serializer.serialize_str(match self {
1163 FormatterProseWrap::None => "none",
1164 FormatterProseWrap::Fill => "fill",
1165 FormatterProseWrap::Sentence => "sentence",
1166 })
1167 }
1168}
1169
1170impl<'de> Deserialize<'de> for FormatterProseWrap {
1171 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1172 where
1173 D: serde::Deserializer<'de>,
1174 {
1175 struct Visitor;
1176
1177 impl serde::de::Visitor<'_> for Visitor {
1178 type Value = FormatterProseWrap;
1179
1180 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1181 formatter.write_str("one of \"none\", \"fill\", \"sentence\", or a boolean")
1182 }
1183
1184 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1185 where
1186 E: serde::de::Error,
1187 {
1188 Ok(if v {
1189 FormatterProseWrap::Fill
1190 } else {
1191 FormatterProseWrap::None
1192 })
1193 }
1194
1195 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1196 where
1197 E: serde::de::Error,
1198 {
1199 match v {
1200 "none" => Ok(FormatterProseWrap::None),
1201 "fill" => Ok(FormatterProseWrap::Fill),
1202 "sentence" => Ok(FormatterProseWrap::Sentence),
1203 _ => Err(E::unknown_variant(v, &["none", "fill", "sentence"])),
1204 }
1205 }
1206 }
1207
1208 deserializer.deserialize_any(Visitor)
1209 }
1210}
1211
1212#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub enum SemanticTokensMode {
1216 Disable,
1218 #[default]
1220 Enable,
1221}
1222
1223#[derive(Debug, Default, Clone, Deserialize)]
1225#[serde(rename_all = "camelCase")]
1226pub struct PreviewFeat {
1227 #[serde(default, deserialize_with = "deserialize_null_default")]
1229 pub browsing: BrowsingPreviewOpts,
1230 #[serde(default, deserialize_with = "deserialize_null_default")]
1232 pub background: BackgroundPreviewOpts,
1233 #[serde(default)]
1235 pub refresh: Option<TaskWhen>,
1236 #[serde(default, deserialize_with = "deserialize_null_default")]
1238 pub partial_rendering: bool,
1239 #[cfg(feature = "preview")]
1241 #[serde(default, deserialize_with = "deserialize_null_default")]
1242 pub invert_colors: PreviewInvertColors,
1243}
1244
1245#[derive(Debug, Default, Clone, Deserialize)]
1247pub struct LintFeat {
1248 pub enabled: Option<bool>,
1250 pub when: Option<TaskWhen>,
1252}
1253
1254impl LintFeat {
1255 pub fn when(&self) -> &TaskWhen {
1257 if matches!(self.enabled, Some(false) | None) {
1258 return &TaskWhen::Never;
1259 }
1260
1261 self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
1262 }
1263}
1264#[derive(Debug, Default, Clone, Deserialize)]
1266#[serde(rename_all = "camelCase")]
1267pub struct OnEnterFeat {
1268 #[serde(default, deserialize_with = "deserialize_null_default")]
1270 pub handle_list: bool,
1271}
1272
1273#[derive(Debug, Default, Clone, Deserialize)]
1275#[serde(rename_all = "camelCase")]
1276pub struct BrowsingPreviewOpts {
1277 pub args: Option<Vec<String>>,
1279}
1280
1281#[derive(Debug, Default, Clone, Deserialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct BackgroundPreviewOpts {
1285 #[serde(default, deserialize_with = "deserialize_null_default")]
1287 pub enabled: bool,
1288 pub args: Option<Vec<String>>,
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Default)]
1296pub struct TypstExtraArgs {
1297 pub root_dir: Option<ImmutPath>,
1299 pub entry: Option<ImmutPath>,
1301 pub inputs: ImmutDict,
1303 pub font: CompileFontArgs,
1305 pub package: CompilePackageArgs,
1307 pub features: Vec<Feature>,
1310 pub pdf_standard: Vec<PdfStandard>,
1313 pub ppi: f32,
1315 pub no_pdf_tags: bool,
1320 pub creation_timestamp: Option<i64>,
1322 pub cert: Option<ImmutPath>,
1324}
1325
1326pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
1327 SemanticTokensOptions {
1328 legend: SemanticTokensLegend {
1329 token_types: TokenType::iter()
1330 .filter(|e| *e != TokenType::None)
1331 .map(Into::into)
1332 .collect(),
1333 token_modifiers: Modifier::iter().map(Into::into).collect(),
1334 },
1335 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
1336 ..SemanticTokensOptions::default()
1337 }
1338}
1339
1340fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1341where
1342 T: Default + Deserialize<'de>,
1343 D: serde::Deserializer<'de>,
1344{
1345 let opt = Option::deserialize(deserializer)?;
1346 Ok(opt.unwrap_or_default())
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351 use super::*;
1352 use serde_json::json;
1353 #[cfg(feature = "preview")]
1354 use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1355
1356 fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1357 temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1358 }
1359
1360 fn good_config(config: &mut Config, update: &JsonValue) {
1361 update_config(config, update).expect("not good");
1362 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1363 }
1364
1365 fn warning_text(config: &Config) -> String {
1366 config
1367 .warnings
1368 .iter()
1369 .map(|warning| warning.as_ref())
1370 .collect::<Vec<_>>()
1371 .join("\n")
1372 }
1373
1374 #[test]
1375 fn test_default_encoding() {
1376 let cc = ConstConfig::default();
1377 assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1378 }
1379
1380 #[test]
1381 fn test_config_update() {
1382 let mut config = Config::default();
1383
1384 let root_path = Path::new(if cfg!(windows) {
1385 "C:\\dummy-root"
1386 } else {
1387 "/dummy-root"
1388 });
1389
1390 let update = json!({
1391 "outputPath": "out",
1392 "exportPdf": "onSave",
1393 "rootPath": root_path,
1394 "semanticTokens": "enable",
1395 "formatterMode": "typstyle",
1396 "typstExtraArgs": ["--root", root_path]
1397 });
1398
1399 good_config(&mut config, &update);
1400
1401 let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1403 if has_source_date_epoch {
1404 let args = config.typst_extra_args.as_mut().unwrap();
1405 assert!(args.creation_timestamp.is_some());
1406 args.creation_timestamp = None;
1407 }
1408
1409 assert_eq!(config.output_path, PathPattern::new("out"));
1410 assert_eq!(config.export_pdf, TaskWhen::OnSave);
1411 assert_eq!(
1412 config.entry_resolver.root_path,
1413 Some(ImmutPath::from(root_path))
1414 );
1415 assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1416 assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1417 assert_eq!(
1418 config.typst_extra_args,
1419 Some(TypstExtraArgs {
1420 root_dir: Some(ImmutPath::from(root_path)),
1421 ppi: 144.0,
1422 ..TypstExtraArgs::default()
1423 })
1424 );
1425 }
1426
1427 #[test]
1428 fn test_namespaced_config() {
1429 let mut config = Config::default();
1430
1431 let update = json!({
1433 "exportPdf": "onSave",
1434 "tinymist": {
1435 "exportPdf": "onType",
1436 }
1437 });
1438
1439 good_config(&mut config, &update);
1440
1441 assert_eq!(config.export_pdf, TaskWhen::OnType);
1442 }
1443
1444 #[test]
1449 fn test_namespaced_config_outside_initialization_options() {
1450 let mut config = Config::default();
1453 let update = json!({
1454 "tinymist": {
1455 "exportPdf": "onType",
1456 }
1457 });
1458 config
1459 .update_by_map(update.as_object().unwrap())
1460 .expect("valid config");
1461
1462 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1463 assert_eq!(config.export_pdf, TaskWhen::OnType);
1464
1465 let values = Config::get_items()
1469 .into_iter()
1470 .map(|item| match item.section.as_deref() {
1471 Some("tinymist") => json!({ "exportTarget": "bundle" }),
1472 _ => JsonValue::Null,
1473 })
1474 .collect::<Vec<_>>();
1475 let mut config = Config::default();
1476 config
1477 .update_by_map(&Config::values_to_map(values))
1478 .expect("valid config");
1479
1480 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1481 assert_eq!(config.export_target, ExportTarget::Bundle);
1482 }
1483
1484 #[test]
1485 fn test_compile_status() {
1486 let mut config = Config::default();
1487
1488 let update = json!({
1489 "compileStatus": "enable",
1490 });
1491 good_config(&mut config, &update);
1492 assert!(config.notify_status);
1493
1494 let update = json!({
1495 "compileStatus": "disable",
1496 });
1497 good_config(&mut config, &update);
1498 assert!(!config.notify_status);
1499 }
1500
1501 #[test]
1502 fn test_all_config_items_are_polled() {
1503 let sections = Config::get_items()
1504 .into_iter()
1505 .filter_map(|item| item.section)
1506 .collect::<Vec<_>>();
1507 let expected = CONFIG_ITEMS
1508 .iter()
1509 .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
1510 .collect::<Vec<_>>();
1511
1512 assert_eq!(sections, expected);
1513 }
1514
1515 #[test]
1516 fn test_polled_restart_scoped_client_options_update_config() {
1517 let values = Config::get_items()
1518 .into_iter()
1519 .map(|item| match item.section.as_deref() {
1520 Some("tinymist.compileStatus") => json!("enable"),
1521 Some("tinymist.triggerSuggest")
1522 | Some("tinymist.triggerParameterHints")
1523 | Some("tinymist.triggerSuggestAndParameterHints")
1524 | Some("tinymist.supportHtmlInMarkdown")
1525 | Some("tinymist.supportClientCodelens")
1526 | Some("tinymist.supportExtendedCodeAction")
1527 | Some("tinymist.customizedShowDocument")
1528 | Some("tinymist.delegateFsRequests") => json!(true),
1529 _ => JsonValue::Null,
1530 })
1531 .collect::<Vec<_>>();
1532
1533 let update = Config::values_to_map(values);
1534 let mut config = Config::default();
1535 config.update_by_map(&update).expect("valid config");
1536
1537 assert!(config.notify_status);
1538 assert!(config.completion.trigger_suggest);
1539 assert!(config.completion.trigger_parameter_hints);
1540 assert!(config.completion.trigger_suggest_and_parameter_hints);
1541 assert!(config.support_html_in_markdown);
1542 assert!(config.support_client_codelens);
1543 assert!(config.extended_code_action);
1544 assert!(config.customized_show_document);
1545 assert!(config.delegate_fs_requests);
1546 }
1547
1548 #[test]
1549 fn test_restart_scoped_client_options_diff() {
1550 let old_config = Config::default();
1551 let mut new_config = Config::default();
1552 let update = json!({
1553 "supportClientCodelens": true,
1554 });
1555
1556 good_config(&mut new_config, &update);
1557
1558 assert_ne!(
1559 old_config.restart_scoped_client_opts(),
1560 new_config.restart_scoped_client_opts()
1561 );
1562 }
1563
1564 #[test]
1565 fn test_config_creation_timestamp() {
1566 type Timestamp = Option<i64>;
1567
1568 fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1569 let mut config = Config::default();
1570
1571 f(&mut config);
1572
1573 let args = config.typst_extra_args;
1574 args.and_then(|args| args.creation_timestamp)
1575 }
1576
1577 let args_timestamp = timestamp(|config| {
1585 let update = json!({
1586 "typstExtraArgs": ["--creation-timestamp", "1234"]
1587 });
1588 good_config(config, &update);
1589 });
1590 assert!(args_timestamp.is_some());
1591
1592 }
1600
1601 #[test]
1602 fn test_typst_extra_args_hint_for_joined_option_value() {
1603 for (joined, option, value) in [
1604 ("--input foo=bar", "--input", "foo=bar"),
1605 ("--pdf-standard ua-1", "--pdf-standard", "ua-1"),
1606 ("--root /workspace", "--root", "/workspace"),
1607 ] {
1608 let mut config = Config::default();
1609 let update = json!({
1610 "typstExtraArgs": [joined]
1611 });
1612
1613 update_config(&mut config, &update).expect("config should recover from bad extra args");
1614
1615 let warnings = warning_text(&config);
1616 let split_form = format!(r#""{option}", "{value}""#);
1617 let equal_form = format!(r#""{option}={value}""#);
1618 for expected in [
1619 "argv array",
1620 joined,
1621 split_form.as_str(),
1622 equal_form.as_str(),
1623 ] {
1624 assert!(
1625 warnings.contains(expected),
1626 "warnings for {joined}: {warnings}"
1627 );
1628 }
1629 }
1630 }
1631
1632 #[test]
1633 fn test_typst_extra_args_no_joined_option_value_hint_for_equal_form_with_spaces() {
1634 let mut config = Config::default();
1635 let update = json!({
1636 "typstExtraArgs": ["--input=a=x y", "--unknown"]
1637 });
1638
1639 update_config(&mut config, &update).expect("config should recover from bad extra args");
1640
1641 let warnings = warning_text(&config);
1642
1643 assert!(!warnings.contains("argv array"), "warnings: {warnings}");
1644 assert!(
1645 warnings.contains("--unknown"),
1646 "unexpected warnings: {warnings}"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_empty_extra_args() {
1652 let mut config = Config::default();
1653 let update = json!({
1654 "typstExtraArgs": []
1655 });
1656
1657 good_config(&mut config, &update);
1658 }
1659
1660 #[test]
1661 fn test_null_args() {
1662 fn test_good_config(path: &str) -> Config {
1663 let mut obj = json!(null);
1664 let path = path.split('.').collect::<Vec<_>>();
1665 for p in path.iter().rev() {
1666 obj = json!({ *p: obj });
1667 }
1668
1669 let mut c = Config::default();
1670 good_config(&mut c, &obj);
1671 c
1672 }
1673
1674 test_good_config("root");
1675 test_good_config("rootPath");
1676 test_good_config("colorTheme");
1677 test_good_config("lint");
1678 test_good_config("customizedShowDocument");
1679 test_good_config("projectResolution");
1680 test_good_config("exportPdf");
1681 test_good_config("exportTarget");
1682 test_good_config("fontPaths");
1683 test_good_config("formatterMode");
1684 test_good_config("formatterPrintWidth");
1685 test_good_config("formatterIndentSize");
1686 test_good_config("formatterProseWrap");
1687 test_good_config("outputPath");
1688 test_good_config("semanticTokens");
1689 test_good_config("delegateFsRequests");
1690 test_good_config("supportHtmlInMarkdown");
1691 test_good_config("supportClientCodelens");
1692 test_good_config("supportExtendedCodeAction");
1693 test_good_config("development");
1694 test_good_config("systemFonts");
1695
1696 test_good_config("completion");
1697 test_good_config("completion.triggerSuggest");
1698 test_good_config("completion.triggerParameterHints");
1699 test_good_config("completion.triggerSuggestAndParameterHints");
1700 test_good_config("completion.triggerOnSnippetPlaceholders");
1701 test_good_config("completion.symbol");
1702 test_good_config("completion.postfix");
1703 test_good_config("completion.postfixUfcs");
1704 test_good_config("completion.postfixUfcsLeft");
1705 test_good_config("completion.postfixUfcsRight");
1706 test_good_config("completion.postfixSnippets");
1707
1708 test_good_config("lint");
1709 test_good_config("lint.enabled");
1710 test_good_config("lint.when");
1711
1712 test_good_config("preview");
1713 test_good_config("preview.browsing");
1714 test_good_config("preview.browsing.args");
1715 test_good_config("preview.background");
1716 test_good_config("preview.background.enabled");
1717 test_good_config("preview.background.args");
1718 test_good_config("preview.refresh");
1719 test_good_config("preview.partialRendering");
1720 #[cfg(feature = "preview")]
1721 let c = test_good_config("preview.invertColors");
1722 #[cfg(feature = "preview")]
1723 assert_eq!(
1724 c.preview.invert_colors,
1725 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1726 );
1727 }
1728
1729 #[test]
1730 fn test_font_opts() {
1731 fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1732 let mut config = Config::default();
1733 if let Some(update) = update {
1734 good_config(&mut config, update);
1735 }
1736
1737 config.font_opts()
1738 }
1739
1740 let font_opts = opts(None);
1741 assert!(!font_opts.ignore_system_fonts);
1742
1743 let font_opts = opts(Some(&json!({})));
1744 assert!(!font_opts.ignore_system_fonts);
1745
1746 let font_opts = opts(Some(&json!({
1747 "typstExtraArgs": []
1748 })));
1749 assert!(!font_opts.ignore_system_fonts);
1750
1751 let font_opts = opts(Some(&json!({
1752 "systemFonts": false,
1753 })));
1754 assert!(font_opts.ignore_system_fonts);
1755
1756 let font_opts = opts(Some(&json!({
1757 "typstExtraArgs": ["--ignore-system-fonts"]
1758 })));
1759 assert!(font_opts.ignore_system_fonts);
1760
1761 let font_opts = opts(Some(&json!({
1762 "systemFonts": true,
1763 "typstExtraArgs": ["--ignore-system-fonts"]
1764 })));
1765 assert!(!font_opts.ignore_system_fonts);
1766 }
1767
1768 #[test]
1769 fn test_preview_opts() {
1770 fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1771 let mut config = Config::default();
1772 if let Some(update) = update {
1773 good_config(&mut config, update);
1774 }
1775
1776 config.preview
1777 }
1778
1779 let preview = opts(Some(&json!({
1780 "preview": {
1781 }
1782 })));
1783 assert_eq!(preview.refresh, None);
1784
1785 let preview = opts(Some(&json!({
1786 "preview": {
1787 "refresh":"onType"
1788 }
1789 })));
1790 assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1791
1792 let preview = opts(Some(&json!({
1793 "preview": {
1794 "refresh":"onSave"
1795 }
1796 })));
1797 assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1798 }
1799
1800 #[test]
1801 fn test_reject_abnormal_root() {
1802 let mut config = Config::default();
1803 let update = json!({
1804 "rootPath": ".",
1805 });
1806
1807 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1808 assert!(err.contains("absolute path"), "unexpected error: {err}");
1809 }
1810
1811 #[test]
1812 fn test_reject_abnormal_root2() {
1813 let mut config = Config::default();
1814 let update = json!({
1815 "typstExtraArgs": ["--root", "."]
1816 });
1817
1818 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1819 assert!(err.contains("absolute path"), "unexpected error: {err}");
1820 }
1821
1822 #[test]
1823 fn test_entry_by_extra_args() {
1824 let simple_config = {
1825 let mut config = Config::default();
1826 let update = json!({
1827 "typstExtraArgs": ["main.typ"]
1828 });
1829
1830 update_config(&mut config, &update).expect("updated");
1832 update_config(&mut config, &update).expect("updated");
1834 config
1835 };
1836 {
1837 let mut config = Config::default();
1838 let update = json!({
1839 "typstExtraArgs": ["main.typ", "main.typ"]
1840 });
1841 update_config(&mut config, &update).unwrap();
1842 let warns = format!("{:?}", config.warnings);
1843 assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1844 assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1845 }
1846 {
1847 let mut config = Config::default();
1848 let update = json!({
1849 "typstExtraArgs": ["main2.typ"],
1850 "tinymist": {
1851 "typstExtraArgs": ["main.typ"]
1852 }
1853 });
1854
1855 update_config(&mut config, &update).expect("updated");
1857 update_config(&mut config, &update).expect("updated");
1859
1860 assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1861 }
1862 }
1863
1864 #[test]
1865 fn test_default_formatting_config() {
1866 let config = Config::default().formatter();
1867 assert!(matches!(config.config, FormatterConfig::Typstyle(_)));
1868 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1869 }
1870
1871 #[test]
1872 fn test_typstyle_formatting_config() {
1873 let config = Config {
1874 formatter_mode: FormatterMode::Typstyle,
1875 ..Config::default()
1876 };
1877 let config = config.formatter();
1878 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1879
1880 let typstyle_config = match config.config {
1881 FormatterConfig::Typstyle(e) => e,
1882 _ => panic!("unexpected configuration of formatter"),
1883 };
1884
1885 assert_eq!(typstyle_config.max_width, 120);
1886 }
1887
1888 #[test]
1889 fn test_typstyle_formatting_config_set_width() {
1890 let config = Config {
1891 formatter_mode: FormatterMode::Typstyle,
1892 formatter_print_width: Some(240),
1893 ..Config::default()
1894 };
1895 let config = config.formatter();
1896 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1897
1898 let typstyle_config = match config.config {
1899 FormatterConfig::Typstyle(e) => e,
1900 _ => panic!("unexpected configuration of formatter"),
1901 };
1902
1903 assert_eq!(typstyle_config.max_width, 240);
1904 }
1905
1906 #[test]
1907 fn test_typstyle_formatting_config_set_tab_spaces() {
1908 let config = Config {
1909 formatter_mode: FormatterMode::Typstyle,
1910 formatter_indent_size: Some(8),
1911 ..Config::default()
1912 };
1913 let config = config.formatter();
1914 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1915
1916 let typstyle_config = match config.config {
1917 FormatterConfig::Typstyle(e) => e,
1918 _ => panic!("unexpected configuration of formatter"),
1919 };
1920
1921 assert_eq!(typstyle_config.tab_spaces, 8);
1922 }
1923
1924 #[test]
1925 #[cfg(feature = "preview")]
1926 fn test_default_preview_config() {
1927 let config = Config::default().preview();
1928 assert!(!config.enable_partial_rendering);
1929 assert_eq!(config.refresh_style, TaskWhen::OnType);
1930 assert_eq!(config.invert_colors, "\"never\"");
1931 }
1932
1933 #[test]
1934 #[cfg(feature = "preview")]
1935 fn test_preview_config() {
1936 let config = Config {
1937 preview: PreviewFeat {
1938 partial_rendering: true,
1939 refresh: Some(TaskWhen::OnSave),
1940 invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1941 ..PreviewFeat::default()
1942 },
1943 ..Config::default()
1944 }
1945 .preview();
1946
1947 assert!(config.enable_partial_rendering);
1948 assert_eq!(config.refresh_style, TaskWhen::OnSave);
1949 assert_eq!(config.invert_colors, "\"auto\"");
1950 }
1951
1952 #[test]
1953 fn test_default_lsp_config_initialize() {
1954 let (conf, err) =
1955 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1956 assert!(err.is_none());
1957 assert!(!conf.const_config.completion_insert_replace_support);
1958 }
1959
1960 #[cfg(feature = "system")]
1961 #[test]
1962 fn test_system_lsp_config_enables_filesystem_path_completion() {
1963 let (conf, err) =
1964 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1965 assert!(err.is_none());
1966 assert!(conf.completion.path_completion_by_filesystem);
1967
1968 let params = InitializeParams {
1969 initialization_options: Some(json!({
1970 "delegateFsRequests": true,
1971 })),
1972 ..InitializeParams::default()
1973 };
1974 let (conf, err) = Config::extract_lsp_params(params, CompileFontArgs::default());
1975 assert!(err.is_none());
1976 assert!(!conf.completion.path_completion_by_filesystem);
1977 }
1978
1979 #[test]
1980 fn test_lsp_config_completion_insert_replace_support() {
1981 let params = InitializeParams {
1982 capabilities: ClientCapabilities {
1983 text_document: Some(TextDocumentClientCapabilities {
1984 completion: Some(CompletionClientCapabilities {
1985 completion_item: Some(CompletionItemCapability {
1986 insert_replace_support: Some(true),
1987 ..CompletionItemCapability::default()
1988 }),
1989 ..CompletionClientCapabilities::default()
1990 }),
1991 ..TextDocumentClientCapabilities::default()
1992 }),
1993 ..ClientCapabilities::default()
1994 },
1995 ..InitializeParams::default()
1996 };
1997
1998 let (conf, err) = Config::extract_lsp_params(params, CompileFontArgs::default());
1999 assert!(err.is_none());
2000 assert!(conf.const_config.completion_insert_replace_support);
2001 }
2002
2003 #[test]
2004 fn test_default_dap_config_initialize() {
2005 let (_conf, err) = Config::extract_dap_params(
2006 dapts::InitializeRequestArguments::default(),
2007 CompileFontArgs::default(),
2008 );
2009 assert!(err.is_none());
2010 }
2011
2012 #[test]
2013 fn test_config_package_path_from_env() {
2014 let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
2015
2016 temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
2017 let (conf, err) =
2018 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
2019 assert!(err.is_none());
2020 let applied_cache_path = conf
2021 .typst_extra_args
2022 .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
2023 assert!(applied_cache_path);
2024 });
2025 }
2026
2027 #[test]
2028 #[cfg(feature = "preview")]
2029 fn test_invert_colors_validation() {
2030 fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
2031 Ok(serde_json::from_str(s)?)
2032 }
2033
2034 assert_eq!(
2035 test(r#""never""#).unwrap(),
2036 PreviewInvertColors::Enum(PreviewInvertColor::Never)
2037 );
2038 assert_eq!(
2039 test(r#""auto""#).unwrap(),
2040 PreviewInvertColors::Enum(PreviewInvertColor::Auto)
2041 );
2042 assert_eq!(
2043 test(r#""always""#).unwrap(),
2044 PreviewInvertColors::Enum(PreviewInvertColor::Always)
2045 );
2046 assert!(test(r#""e""#).is_err());
2047
2048 assert_eq!(
2049 test(r#"{"rest": "never"}"#).unwrap(),
2050 PreviewInvertColors::Object(PreviewInvertColorObject {
2051 image: PreviewInvertColor::Never,
2052 rest: PreviewInvertColor::Never,
2053 })
2054 );
2055 assert_eq!(
2056 test(r#"{"image": "always"}"#).unwrap(),
2057 PreviewInvertColors::Object(PreviewInvertColorObject {
2058 image: PreviewInvertColor::Always,
2059 rest: PreviewInvertColor::Never,
2060 })
2061 );
2062 assert_eq!(
2063 test(r#"{}"#).unwrap(),
2064 PreviewInvertColors::Object(PreviewInvertColorObject {
2065 image: PreviewInvertColor::Never,
2066 rest: PreviewInvertColor::Never,
2067 })
2068 );
2069 assert_eq!(
2070 test(r#"{"unknown": "ovo"}"#).unwrap(),
2071 PreviewInvertColors::Object(PreviewInvertColorObject {
2072 image: PreviewInvertColor::Never,
2073 rest: PreviewInvertColor::Never,
2074 })
2075 );
2076 assert!(test(r#"{"image": "e"}"#).is_err());
2077 }
2078}