tinymist/
config.rs

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
41// region Configuration Items
42const 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// endregion Configuration Items
67
68/// The user configuration read from the editor.
69///
70/// Note: `Config::default` is intentionally to be "pure" and not to be
71/// affected by system environment variables.
72/// To get the configuration with system defaults, use [`Config::new`] instead.
73#[derive(Debug, Default, Clone)]
74pub struct Config {
75    /// Constant configuration during session.
76    pub const_config: ConstConfig,
77    /// Constant DAP-specific configuration during session.
78    pub const_dap_config: ConstDapConfig,
79
80    /// Whether to delegate file system accesses to the client.
81    pub delegate_fs_requests: bool,
82    /// Whether to send show document requests with customized notification.
83    pub customized_show_document: bool,
84    /// Whether the configuration can have a default entry path.
85    pub has_default_entry_path: bool,
86    /// Whether to notify the status to the editor.
87    pub notify_status: bool,
88    /// Whether to remove HTML from markup content in responses.
89    pub support_html_in_markdown: bool,
90    /// Whether to utilize the extended `tinymist.resolveCodeAction` at client
91    /// side.
92    pub extended_code_action: bool,
93    /// Whether to run the server in development mode.
94    pub development: bool,
95
96    /// The preferred color theme for rendering.
97    pub color_theme: Option<String>,
98    /// The entry resolver.
99    pub entry_resolver: EntryResolver,
100    /// The `sys.inputs` passed to the typst compiler.
101    pub lsp_inputs: ImmutDict,
102    /// The arguments about periscope rendering in hover window.
103    pub periscope_args: Option<PeriscopeArgs>,
104    /// The extra typst arguments passed to the language server.
105    pub typst_extra_args: Option<TypstExtraArgs>,
106    /// The dynamic configuration for semantic tokens.
107    pub semantic_tokens: SemanticTokensMode,
108
109    /// Tinymist's completion features.
110    pub completion: CompletionFeat,
111    /// Tinymist's preview features.
112    pub preview: PreviewFeat,
113    /// Tinymist's lint features.
114    pub lint: LintFeat,
115    /// Tinymist's on-enter features.
116    pub on_enter: OnEnterFeat,
117
118    /// Specifies the cli font options
119    pub font_opts: CompileFontArgs,
120    /// Specifies the font paths
121    pub font_paths: Vec<PathBuf>,
122    /// Computed fonts based on configuration.
123    pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
124    /// Whether to use system fonts.
125    pub system_fonts: Option<bool>,
126
127    /// Computed watch access model based on configuration.
128    pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
129    /// Computed access model based on configuration.
130    pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
131
132    /// Tinymist's default export target.
133    pub export_target: ExportTarget,
134    /// The mode of PDF export.
135    pub export_pdf: TaskWhen,
136    /// The output directory for PDF export.
137    pub output_path: PathPattern,
138
139    /// Dynamic configuration for the experimental formatter.
140    pub formatter_mode: FormatterMode,
141    /// Sets the print width for the formatter, which is a **soft limit** of
142    /// characters per line. See [the definition of *Print Width*](https://prettier.io/docs/en/options.html#print-width).
143    pub formatter_print_width: Option<u32>,
144    /// Sets the indent size (using space) for the formatter.
145    pub formatter_indent_size: Option<u32>,
146    /// Sets the hard line wrapping mode for the formatter.
147    pub formatter_prose_wrap: Option<bool>,
148    /// The warnings during configuration update.
149    pub warnings: Vec<CowStr>,
150}
151
152impl Config {
153    /// Creates a new configuration with system defaults.
154    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    /// Creates a new configuration from the LSP initialization parameters.
176    ///
177    /// The function has side effects:
178    /// - Getting environment variables.
179    /// - Setting the locale.
180    pub fn extract_lsp_params(
181        params: InitializeParams,
182        font_args: CompileFontArgs,
183    ) -> (Self, Option<ResponseError>) {
184        // Initialize configurations
185        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)] // `params.root_path` is marked as deprecated
191            None => params
192                .root_uri
193                .as_ref()
194                .map(|uri| ImmutPath::from(url_to_path(uri)))
195                .or_else(|| Some(Path::new(&params.root_path.as_ref()?).into()))
196                .into_iter()
197                .collect(),
198        };
199        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
200
201        // Sets locale as soon as possible
202        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    /// Creates a new configuration from the DAP initialization parameters.
214    ///
215    /// The function has side effects:
216    /// - Getting environment variables.
217    /// - Setting the locale.
218    pub fn extract_dap_params(
219        params: dapts::InitializeRequestArguments,
220        font_args: CompileFontArgs,
221    ) -> (Self, Option<ResponseError>) {
222        // This is reliable in DAP context.
223        let cwd = std::env::current_dir()
224            .expect("failed to get current directory")
225            .into();
226
227        // Initialize configurations
228        let roots = vec![cwd];
229        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
230        config.const_dap_config = ConstDapConfig::from(&params);
231
232        // Sets locale as soon as possible
233        if let Some(locale) = config.const_config.locale.as_ref() {
234            tinymist_l10n::set_locale(locale);
235        }
236
237        (config, None)
238    }
239
240    /// Gets configuration descriptors to request configuration sections from
241    /// the client.
242    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    /// Converts config values to a map object.
254    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    /// Updates (and validates) the configuration by a JSON object.
268    ///
269    /// The config may be broken if the update is invalid. Please clone the
270    /// configuration before updating and revert if the update fails.
271    pub fn update(&mut self, update: &JsonValue) -> Result<()> {
272        if let JsonValue::Object(update) = update {
273            self.update_by_map(update)?;
274
275            // Configurations in the tinymist namespace take precedence.
276            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    /// Updates (and validates) the configuration by a map object.
291    ///
292    /// The config may be broken if the update is invalid. Please clone the
293    /// configuration before updating and revert if the update fails.
294    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                            // Only ignore null returns. Some editors may send null values when
308                            // the configuration is not set, e.g. Zed.
309                            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        // periscope_args
377        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                // Even if the list is none, it should be parsed since we have env vars to
417                // retrieve.
418                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                        // Still try to parse the arguments to get the environment variables.
434                        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            // todo: the command.root may be not absolute
444            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    /// Validates the configuration.
494    pub fn validate(&self) -> Result<()> {
495        self.entry_resolver.validate()?;
496
497        Ok(())
498    }
499
500    /// Gets the formatter configuration.
501    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    /// Gets the preview configuration.
529    #[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    /// Gets the export task configuration.
540    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    /// Gets the export configuration.
549    #[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            // todo: we only have `exportPdf` for now
555            // task: match self.export_target {
556            //     ExportTarget::Paged => ProjectTask::ExportPdf(ExportPdfTask {
557            //         export,
558            //         pdf_standards: vec![],
559            //         creation_timestamp: compile_config.determine_creation_timestamp(),
560            //     }),
561            //     ExportTarget::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
562            // },
563            task: ProjectTask::ExportPdf(ExportPdfTask {
564                export,
565                pages: None, // todo: set pages
566                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    /// Determines the font options.
576    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    /// Determines the package options.
608    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    /// Determines the font resolver.
616    pub fn fonts(&self) -> Arc<FontResolverImpl> {
617        // todo: on font resolving failure, downgrade to a fake font book
618        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    /// Determines the `sys.inputs` for the entry file.
632    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    /// Determines the typst features
657    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    /// Determines the pdf standards.
663    pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
664        Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
665    }
666
667    /// Determines the no pdf tags.
668    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    /// Determines the ppi.
675    pub fn ppi(&self) -> Option<f32> {
676        Some(self.typst_extra_args.as_ref()?.ppi)
677    }
678
679    /// Determines the creation timestamp.
680    pub fn creation_timestamp(&self) -> Option<i64> {
681        self.typst_extra_args.as_ref()?.creation_timestamp
682    }
683
684    /// Determines the certification path.
685    pub fn certification_path(&self) -> Option<ImmutPath> {
686        self.typst_extra_args.as_ref()?.cert.clone()
687    }
688
689    /// Applies the primary options related to compilation.
690    #[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/// Configuration set at initialization that won't change within a single
755/// session.
756#[derive(Debug, Clone)]
757pub struct ConstConfig {
758    /// Determined position encoding, either UTF-8 or UTF-16.
759    /// Defaults to UTF-16 if not specified.
760    pub position_encoding: PositionEncoding,
761    /// Allow dynamic registration of configuration changes.
762    pub cfg_change_registration: bool,
763    /// Allow notifying workspace/didRenameFiles
764    pub notify_will_rename_files: bool,
765    /// Allow dynamic registration of semantic tokens.
766    pub tokens_dynamic_registration: bool,
767    /// Allow overlapping tokens.
768    pub tokens_overlapping_token_support: bool,
769    /// Allow multiline tokens.
770    pub tokens_multiline_token_support: bool,
771    /// Allow line folding on documents.
772    pub doc_line_folding_only: bool,
773    /// Allow dynamic registration of document formatting.
774    pub doc_fmt_dynamic_registration: bool,
775    /// The locale of the editor.
776    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        // const DEFAULT_ENCODING: &[PositionEncodingKind] =
788        // &[PositionEncodingKind::UTF16];
789
790        // todo: respect position encoding.
791        let position_encoding = {
792            // let general = params.capabilities.general.as_ref();
793            // let encodings = try_(||
794            // Some(general?.position_encodings.as_ref()?.as_slice()));
795            // let encodings = encodings.unwrap_or(DEFAULT_ENCODING);
796
797            // if encodings.contains(&PositionEncodingKind::UTF8) {
798            //     PositionEncoding::Utf8
799            // } else {
800            //     PositionEncoding::Utf16
801            // }
802            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
843/// Determines in what format paths are specified. The default is `path`, which
844/// is the native format.
845pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
846
847/// Configuration set at initialization that won't change within a single DAP
848/// session.
849#[derive(Debug, Clone)]
850pub struct ConstDapConfig {
851    /// The format of paths.
852    pub path_format: DapPathFormat,
853    /// Whether lines start at 1.
854    pub lines_start_at1: bool,
855    /// Whether columns start at 1.
856    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/// The mode of the formatter.
876#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
877#[serde(rename_all = "camelCase")]
878pub enum FormatterMode {
879    /// Disable the formatter.
880    #[default]
881    Disable,
882    /// Use `typstyle` formatter.
883    Typstyle,
884    /// Use `typstfmt` formatter.
885    Typstfmt,
886}
887
888/// The mode of semantic tokens.
889#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub enum SemanticTokensMode {
892    /// Disable the semantic tokens.
893    Disable,
894    /// Enable the semantic tokens.
895    #[default]
896    Enable,
897}
898
899/// The preview features.
900#[derive(Debug, Default, Clone, Deserialize)]
901#[serde(rename_all = "camelCase")]
902pub struct PreviewFeat {
903    /// The browsing preview options.
904    #[serde(default, deserialize_with = "deserialize_null_default")]
905    pub browsing: BrowsingPreviewOpts,
906    /// The background preview options.
907    #[serde(default, deserialize_with = "deserialize_null_default")]
908    pub background: BackgroundPreviewOpts,
909    /// When to refresh the preview.
910    #[serde(default)]
911    pub refresh: Option<TaskWhen>,
912    /// Whether to enable partial rendering.
913    #[serde(default, deserialize_with = "deserialize_null_default")]
914    pub partial_rendering: bool,
915    /// Invert colors for the preview.
916    #[cfg(feature = "preview")]
917    #[serde(default, deserialize_with = "deserialize_null_default")]
918    pub invert_colors: PreviewInvertColors,
919}
920
921/// The lint features.
922#[derive(Debug, Default, Clone, Deserialize)]
923pub struct LintFeat {
924    /// Whether to enable linting.
925    pub enabled: Option<bool>,
926    /// When to trigger the lint checks.
927    pub when: Option<TaskWhen>,
928}
929
930impl LintFeat {
931    /// When to trigger the lint checks.
932    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/// The lint features.
941#[derive(Debug, Default, Clone, Deserialize)]
942#[serde(rename_all = "camelCase")]
943pub struct OnEnterFeat {
944    /// Whether to handle list.
945    #[serde(default, deserialize_with = "deserialize_null_default")]
946    pub handle_list: bool,
947}
948
949/// Options for browsing preview.
950#[derive(Debug, Default, Clone, Deserialize)]
951#[serde(rename_all = "camelCase")]
952pub struct BrowsingPreviewOpts {
953    /// The arguments for the `tinymist.startDefaultPreview` command.
954    pub args: Option<Vec<String>>,
955}
956
957/// Options for background preview.
958#[derive(Debug, Default, Clone, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct BackgroundPreviewOpts {
961    /// Whether to run the preview in the background.
962    #[serde(default, deserialize_with = "deserialize_null_default")]
963    pub enabled: bool,
964    /// The arguments for the background preview.
965    pub args: Option<Vec<String>>,
966}
967
968/// The extra typst arguments passed to the language server. You can pass any
969/// arguments as you like, and we will try to follow behaviors of the **same
970/// version** of typst-cli.
971#[derive(Debug, Clone, PartialEq, Default)]
972pub struct TypstExtraArgs {
973    /// The root directory for the compilation routine.
974    pub root_dir: Option<ImmutPath>,
975    /// The path to the entry.
976    pub entry: Option<ImmutPath>,
977    /// The additional input arguments to compile the entry file.
978    pub inputs: ImmutDict,
979    /// The additional font paths.
980    pub font: CompileFontArgs,
981    /// The package related arguments.
982    pub package: CompilePackageArgs,
983    /// One (or multiple comma-separated) PDF standards that Typst will enforce
984    /// conformance with.
985    pub features: Vec<Feature>,
986    /// One (or multiple comma-separated) PDF standards that Typst will enforce
987    /// conformance with.
988    pub pdf_standard: Vec<PdfStandard>,
989    /// The PPI (pixels per inch) to use for PNG export.
990    pub ppi: f32,
991    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
992    /// document is written to provide a baseline of accessibility. In some
993    /// circumstances (for example when trying to reduce the size of a document)
994    /// it can be desirable to disable tagged PDF.
995    pub no_pdf_tags: bool,
996    /// The creation timestamp for various outputs (in seconds).
997    pub creation_timestamp: Option<i64>,
998    /// The path to the certification file.
999    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        // Nix specifies this environment variable when testing.
1069        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        // Emacs uses a shared configuration object for all language servers.
1099        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        // assert!(timestamp(|_| {}).is_none());
1142        // assert!(timestamp(|config| {
1143        //     let update = json!({});
1144        //     good_config(&mut config, &update);
1145        // })
1146        // .is_none());
1147
1148        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        // todo: concurrent get/set env vars is unsafe
1157        //     std::env::set_var("SOURCE_DATE_EPOCH", "1234");
1158        //     let env_timestamp = timestamp(|config| {
1159        //         update_config(&mut config, &json!({})).unwrap();
1160        //     });
1161
1162        //     assert_eq!(args_timestamp, env_timestamp);
1163    }
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            // It should be able to resolve the entry file from the extra arguments.
1345            update_config(&mut config, &update).expect("updated");
1346            // Passing it twice doesn't affect the result.
1347            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            // It should be able to resolve the entry file from the extra arguments.
1370            update_config(&mut config, &update).expect("updated");
1371            // Passing it twice doesn't affect the result.
1372            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}