tinymist/
config.rs

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
46// region Configuration Items
47const 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// endregion Configuration Items
81
82/// The user configuration read from the editor.
83///
84/// Note: `Config::default` is intentionally to be "pure" and not to be
85/// affected by system environment variables.
86/// To get the configuration with system defaults, use [`Config::new`] instead.
87#[derive(Debug, Default, Clone)]
88pub struct Config {
89    /// Constant configuration during session.
90    pub const_config: ConstConfig,
91    /// Constant DAP-specific configuration during session.
92    pub const_dap_config: ConstDapConfig,
93
94    /// Whether to delegate file system accesses to the client.
95    pub delegate_fs_requests: bool,
96    /// Whether to send show document requests with customized notification.
97    pub customized_show_document: bool,
98    /// Whether the configuration can have a default entry path.
99    pub has_default_entry_path: bool,
100    /// Whether to notify the status to the editor.
101    pub notify_status: bool,
102    /// Whether to remove HTML from markup content in responses.
103    pub support_html_in_markdown: bool,
104    /// Whether the client has a handler for client-side code lenses.
105    /// When true, the server uses the `tinymist.runCodeLens` command and lets
106    /// the client handle code lens execution. When false, the server provides
107    /// direct export commands instead of client-side code lenses.
108    pub support_client_codelens: bool,
109    /// Whether to utilize the extended `tinymist.resolveCodeAction` at client
110    /// side.
111    pub extended_code_action: bool,
112    /// Whether to run the server in development mode.
113    pub development: bool,
114    /// Whether to run the server in syntax-only mode.
115    pub syntax_only: bool,
116
117    /// The preferred color theme for rendering.
118    pub color_theme: Option<String>,
119    /// The entry resolver.
120    pub entry_resolver: EntryResolver,
121    /// The `sys.inputs` passed to the typst compiler.
122    pub lsp_inputs: ImmutDict,
123    /// The arguments about periscope rendering in hover window.
124    pub periscope_args: Option<PeriscopeArgs>,
125    /// The extra typst arguments passed to the language server.
126    pub typst_extra_args: Option<TypstExtraArgs>,
127    /// The dynamic configuration for semantic tokens.
128    pub semantic_tokens: SemanticTokensMode,
129
130    /// Tinymist's completion features.
131    pub completion: CompletionFeat,
132    /// Tinymist's preview features.
133    pub preview: PreviewFeat,
134    /// Tinymist's lint features.
135    pub lint: LintFeat,
136    /// Tinymist's on-enter features.
137    pub on_enter: OnEnterFeat,
138
139    /// Specifies the cli font options
140    pub font_opts: CompileFontArgs,
141    /// Specifies the font paths
142    pub font_paths: Vec<PathBuf>,
143    /// Computed fonts based on configuration.
144    pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
145    /// Whether to use system fonts.
146    pub system_fonts: Option<bool>,
147
148    /// Computed watch access model based on configuration.
149    pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
150    /// Computed access model based on configuration.
151    pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
152
153    /// Tinymist's default export target.
154    pub export_target: ExportTarget,
155    /// The mode of PDF export.
156    pub export_pdf: TaskWhen,
157    /// The output directory for PDF export.
158    pub output_path: PathPattern,
159
160    /// Dynamic configuration for the experimental formatter.
161    pub formatter_mode: FormatterMode,
162    /// Sets the print width for the formatter, which is a **soft limit** of
163    /// characters per line. See [the definition of *Print Width*](https://prettier.io/docs/en/options.html#print-width).
164    pub formatter_print_width: Option<u32>,
165    /// Sets the indent size (using space) for the formatter.
166    pub formatter_indent_size: Option<u32>,
167    /// Sets the hard line wrapping mode for the formatter.
168    pub formatter_prose_wrap: Option<FormatterProseWrap>,
169    /// The warnings during configuration update.
170    pub warnings: Vec<CowStr>,
171}
172
173/// Client options whose changes are applied through a project restart boundary.
174#[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    /// Creates a new configuration with system defaults.
189    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    /// Creates a new configuration from the LSP initialization parameters.
223    ///
224    /// The function has side effects:
225    /// - Getting environment variables.
226    /// - Setting the locale.
227    pub fn extract_lsp_params(
228        params: InitializeParams,
229        font_args: CompileFontArgs,
230    ) -> (Self, Option<ResponseError>) {
231        // Initialize configurations
232        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)] // `params.root_path` is marked as deprecated
238            None => params
239                .root_uri
240                .as_ref()
241                .map(|uri| ImmutPath::from(url_to_path(uri)))
242                .or_else(|| Some(Path::new(&params.root_path.as_ref()?).into()))
243                .into_iter()
244                .collect(),
245        };
246        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
247
248        // Sets locale as soon as possible
249        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    /// Creates a new configuration from the DAP initialization parameters.
262    ///
263    /// The function has side effects:
264    /// - Getting environment variables.
265    /// - Setting the locale.
266    pub fn extract_dap_params(
267        params: dapts::InitializeRequestArguments,
268        font_args: CompileFontArgs,
269    ) -> (Self, Option<ResponseError>) {
270        // This is reliable in DAP context.
271        let cwd = std::env::current_dir()
272            .expect("failed to get current directory")
273            .into();
274
275        // Initialize configurations
276        let roots = vec![cwd];
277        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
278        config.const_dap_config = ConstDapConfig::from(&params);
279
280        // Sets locale as soon as possible
281        if let Some(locale) = config.const_config.locale.as_ref() {
282            tinymist_l10n::set_locale(locale);
283        }
284
285        (config, None)
286    }
287
288    /// Gets configuration descriptors to request configuration sections from
289    /// the client.
290    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    /// Converts config values to a map object.
302    ///
303    /// The server asks for both the `tinymist.<key>` and the `<key>` section of
304    /// every setting, and a client may answer the whole `tinymist` section with
305    /// a namespaced object. All of those describe settings in the same
306    /// namespace, so they are merged into the flat shape that
307    /// [`Config::update_by_map`] reads, keeping the precedence that the
308    /// namespace has over top-level settings.
309    ///
310    /// The answer of a single `tinymist.<key>` section wins over the namespace
311    /// object: the client resolved it for that key, and some clients normalize
312    /// it there but not inside the object, e.g. VS Code substitutes editor
313    /// variables in the former only.
314    pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
315        // The object answered for the whole `tinymist` section.
316        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        // Settings the client did not resolve per key come from the namespace
337        // object, which is the only answer a pure eglot client gives.
338        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    /// Updates (and validates) the configuration by a JSON object.
351    ///
352    /// The config may be broken if the update is invalid. Please clone the
353    /// configuration before updating and revert if the update fails.
354    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    /// Unpacks a `tinymist`-namespaced configuration object.
367    ///
368    /// The two forms documented for clients, `{"exportTarget": "bundle"}` and
369    /// `{"tinymist": {"exportTarget": "bundle"}}`, describe the same settings,
370    /// so they are flattened into one shape here.
371    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        // Configurations in the tinymist namespace take precedence.
377        let mut flat = update.clone();
378        flat.extend(namespaced.clone());
379
380        Cow::Owned(flat)
381    }
382
383    /// Updates (and validates) the configuration by a map object.
384    ///
385    /// The config may be broken if the update is invalid. Please clone the
386    /// configuration before updating and revert if the update fails.
387    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                            // Only ignore null returns. Some editors may send null values when
404                            // the configuration is not set, e.g. Zed.
405                            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        // periscope_args
494        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                // Even if the list is none, it should be parsed since we have env vars to
525                // retrieve.
526                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                        // Still try to parse the arguments to get the environment variables.
546                        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            // todo: the command.root may be not absolute
556            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    /// Validates the configuration.
607    pub fn validate(&self) -> Result<()> {
608        self.entry_resolver.validate()?;
609
610        Ok(())
611    }
612
613    /// Configures the syntax-only mode.
614    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    /// Gets the formatter configuration.
625    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        // `typstfmt` only distinguishes whether text is wrapped at all, so both
630        // `fill` and `sentence` map to its line wrapping.
631        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    /// Gets the preview configuration.
656    #[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    /// Gets the export task configuration.
668    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    /// Gets the export configuration.
677    #[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            // todo: we only have `exportPdf` for now
683            // task: match self.export_target {
684            //     ExportTarget::Paged => ProjectTask::ExportPdf(ExportPdfTask {
685            //         export,
686            //         pdf_standards: vec![],
687            //         creation_timestamp: compile_config.determine_creation_timestamp(),
688            //     }),
689            //     ExportTarget::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
690            // },
691            task: ProjectTask::ExportPdf(ExportPdfTask {
692                export,
693                pages: None, // todo: set pages
694                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    /// Determines the font options.
704    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    /// Determines the package options.
736    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    /// Determines the font resolver.
744    pub fn fonts(&self) -> Arc<FontResolverImpl> {
745        // todo: on font resolving failure, downgrade to a fake font book
746        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    /// Determines the `sys.inputs` for the entry file.
760    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    /// Determines the typst features
785    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    /// Determines the pdf standards.
791    pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
792        Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
793    }
794
795    /// Determines the no pdf tags.
796    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    /// Determines the ppi.
803    pub fn ppi(&self) -> Option<f32> {
804        Some(self.typst_extra_args.as_ref()?.ppi)
805    }
806
807    /// Determines the creation timestamp.
808    pub fn creation_timestamp(&self) -> Option<i64> {
809        self.typst_extra_args.as_ref()?.creation_timestamp
810    }
811
812    /// Determines the certification path.
813    pub fn certification_path(&self) -> Option<ImmutPath> {
814        self.typst_extra_args.as_ref()?.cert.clone()
815    }
816
817    /// Applies the primary options related to compilation.
818    #[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            // server
835            self.syntax_only,
836            // typst library
837            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            // typst package
851            self.certification_path(),
852            self.package_opts(),
853            // typst font
854            self.system_fonts,
855            self.font_opts(),
856            self.creation_timestamp(),
857            // typst root
858            self.entry_resolver
859                .root(self.entry_resolver.resolve_default().as_ref()),
860        )
861    }
862
863    /// Returns the client options that require a project restart when changed.
864    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/// Configuration set at initialization that won't change within a single
990/// session.
991#[derive(Debug, Clone)]
992pub struct ConstConfig {
993    /// Determined position encoding, either UTF-8 or UTF-16.
994    /// Defaults to UTF-16 if not specified.
995    pub position_encoding: PositionEncoding,
996    /// Allow dynamic registration of configuration changes.
997    pub cfg_change_registration: bool,
998    /// Allow notifying workspace/didRenameFiles
999    pub notify_will_rename_files: bool,
1000    /// Allow dynamic registration of semantic tokens.
1001    pub tokens_dynamic_registration: bool,
1002    /// Allow overlapping tokens.
1003    pub tokens_overlapping_token_support: bool,
1004    /// Allow multiline tokens.
1005    pub tokens_multiline_token_support: bool,
1006    /// Allow line folding on documents.
1007    pub doc_line_folding_only: bool,
1008    /// Allow dynamic registration of document formatting.
1009    pub doc_fmt_dynamic_registration: bool,
1010    /// Allow insert/replace text edits in completion items.
1011    pub completion_insert_replace_support: bool,
1012    /// The locale of the editor.
1013    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        // const DEFAULT_ENCODING: &[PositionEncodingKind] =
1025        // &[PositionEncodingKind::UTF16];
1026
1027        // todo: respect position encoding.
1028        let position_encoding = {
1029            // let general = params.capabilities.general.as_ref();
1030            // let encodings = try_(||
1031            // Some(general?.position_encodings.as_ref()?.as_slice()));
1032            // let encodings = encodings.unwrap_or(DEFAULT_ENCODING);
1033
1034            // if encodings.contains(&PositionEncodingKind::UTF8) {
1035            //     PositionEncoding::Utf8
1036            // } else {
1037            //     PositionEncoding::Utf16
1038            // }
1039            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
1085/// Determines in what format paths are specified. The default is `path`, which
1086/// is the native format.
1087pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
1088
1089/// Configuration set at initialization that won't change within a single DAP
1090/// session.
1091#[derive(Debug, Clone)]
1092pub struct ConstDapConfig {
1093    /// The format of paths.
1094    pub path_format: DapPathFormat,
1095    /// Whether lines start at 1.
1096    pub lines_start_at1: bool,
1097    /// Whether columns start at 1.
1098    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/// The mode of the formatter.
1118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub enum FormatterMode {
1121    /// Disable the formatter.
1122    Disable,
1123    /// Use `typstyle` formatter.
1124    #[default]
1125    Typstyle,
1126    /// Use `typstfmt` formatter.
1127    Typstfmt,
1128}
1129
1130/// How the formatter reflows prose inside markup.
1131///
1132/// This models `typstyle`'s text wrapping modes. It also accepts the boolean
1133/// form, which predates the wrapping modes: `true` means
1134/// [`FormatterProseWrap::Fill`] and `false` means [`FormatterProseWrap::None`].
1135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1136pub enum FormatterProseWrap {
1137    /// Keep the original line breaks and spaces.
1138    #[default]
1139    None,
1140    /// Wrap prose to fit within the configured print width.
1141    Fill,
1142    /// Place each sentence on its own line.
1143    Sentence,
1144}
1145
1146impl FormatterProseWrap {
1147    /// Converts this setting to the `typstyle` text wrapping mode.
1148    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/// The mode of semantic tokens.
1213#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub enum SemanticTokensMode {
1216    /// Disable the semantic tokens.
1217    Disable,
1218    /// Enable the semantic tokens.
1219    #[default]
1220    Enable,
1221}
1222
1223/// The preview features.
1224#[derive(Debug, Default, Clone, Deserialize)]
1225#[serde(rename_all = "camelCase")]
1226pub struct PreviewFeat {
1227    /// The browsing preview options.
1228    #[serde(default, deserialize_with = "deserialize_null_default")]
1229    pub browsing: BrowsingPreviewOpts,
1230    /// The background preview options.
1231    #[serde(default, deserialize_with = "deserialize_null_default")]
1232    pub background: BackgroundPreviewOpts,
1233    /// When to refresh the preview.
1234    #[serde(default)]
1235    pub refresh: Option<TaskWhen>,
1236    /// Whether to enable partial rendering.
1237    #[serde(default, deserialize_with = "deserialize_null_default")]
1238    pub partial_rendering: bool,
1239    /// Invert colors for the preview.
1240    #[cfg(feature = "preview")]
1241    #[serde(default, deserialize_with = "deserialize_null_default")]
1242    pub invert_colors: PreviewInvertColors,
1243}
1244
1245/// The lint features.
1246#[derive(Debug, Default, Clone, Deserialize)]
1247pub struct LintFeat {
1248    /// Whether to enable linting.
1249    pub enabled: Option<bool>,
1250    /// When to trigger the lint checks.
1251    pub when: Option<TaskWhen>,
1252}
1253
1254impl LintFeat {
1255    /// When to trigger the lint checks.
1256    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/// The lint features.
1265#[derive(Debug, Default, Clone, Deserialize)]
1266#[serde(rename_all = "camelCase")]
1267pub struct OnEnterFeat {
1268    /// Whether to handle list.
1269    #[serde(default, deserialize_with = "deserialize_null_default")]
1270    pub handle_list: bool,
1271}
1272
1273/// Options for browsing preview.
1274#[derive(Debug, Default, Clone, Deserialize)]
1275#[serde(rename_all = "camelCase")]
1276pub struct BrowsingPreviewOpts {
1277    /// The arguments for the `tinymist.startDefaultPreview` command.
1278    pub args: Option<Vec<String>>,
1279}
1280
1281/// Options for background preview.
1282#[derive(Debug, Default, Clone, Deserialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct BackgroundPreviewOpts {
1285    /// Whether to run the preview in the background.
1286    #[serde(default, deserialize_with = "deserialize_null_default")]
1287    pub enabled: bool,
1288    /// The arguments for the background preview.
1289    pub args: Option<Vec<String>>,
1290}
1291
1292/// The extra typst arguments passed to the language server. You can pass any
1293/// arguments as you like, and we will try to follow behaviors of the **same
1294/// version** of typst-cli.
1295#[derive(Debug, Clone, PartialEq, Default)]
1296pub struct TypstExtraArgs {
1297    /// The root directory for the compilation routine.
1298    pub root_dir: Option<ImmutPath>,
1299    /// The path to the entry.
1300    pub entry: Option<ImmutPath>,
1301    /// The additional input arguments to compile the entry file.
1302    pub inputs: ImmutDict,
1303    /// The additional font paths.
1304    pub font: CompileFontArgs,
1305    /// The package related arguments.
1306    pub package: CompilePackageArgs,
1307    /// One (or multiple comma-separated) PDF standards that Typst will enforce
1308    /// conformance with.
1309    pub features: Vec<Feature>,
1310    /// One (or multiple comma-separated) PDF standards that Typst will enforce
1311    /// conformance with.
1312    pub pdf_standard: Vec<PdfStandard>,
1313    /// The PPI (pixels per inch) to use for PNG export.
1314    pub ppi: f32,
1315    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
1316    /// document is written to provide a baseline of accessibility. In some
1317    /// circumstances (for example when trying to reduce the size of a document)
1318    /// it can be desirable to disable tagged PDF.
1319    pub no_pdf_tags: bool,
1320    /// The creation timestamp for various outputs (in seconds).
1321    pub creation_timestamp: Option<i64>,
1322    /// The path to the certification file.
1323    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        // Nix specifies this environment variable when testing.
1402        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        // Emacs uses a shared configuration object for all language servers.
1432        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    /// Regression test for issue #2714: a `tinymist`-namespaced object must be
1445    /// honored outside `initializationOptions` as well, both when it arrives in
1446    /// a `workspace/didChangeConfiguration` payload and when it is answered for
1447    /// the `tinymist` section of a `workspace/configuration` pull.
1448    #[test]
1449    fn test_namespaced_config_outside_initialization_options() {
1450        // The notification path, which reads the payload through
1451        // `update_by_map`.
1452        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        // The pull path, where the answer of the `tinymist` section is a
1466        // namespaced object and reaches `update_by_map` through
1467        // `values_to_map`.
1468        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        // assert!(timestamp(|_| {}).is_none());
1578        // assert!(timestamp(|config| {
1579        //     let update = json!({});
1580        //     good_config(&mut config, &update);
1581        // })
1582        // .is_none());
1583
1584        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        // todo: concurrent get/set env vars is unsafe
1593        //     std::env::set_var("SOURCE_DATE_EPOCH", "1234");
1594        //     let env_timestamp = timestamp(|config| {
1595        //         update_config(&mut config, &json!({})).unwrap();
1596        //     });
1597
1598        //     assert_eq!(args_timestamp, env_timestamp);
1599    }
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            // It should be able to resolve the entry file from the extra arguments.
1831            update_config(&mut config, &update).expect("updated");
1832            // Passing it twice doesn't affect the result.
1833            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            // It should be able to resolve the entry file from the extra arguments.
1856            update_config(&mut config, &update).expect("updated");
1857            // Passing it twice doesn't affect the result.
1858            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}