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                features: args.features,
452                creation_timestamp: args.creation_timestamp,
453                cert: args.cert.as_deref().map(From::from),
454            });
455        }
456
457        self.entry_resolver.root_path =
458            try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
459                self.typst_extra_args
460                    .as_ref()
461                    .and_then(|e| e.root_dir.clone())
462            });
463        self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
464        self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
465        self.lsp_inputs = {
466            let mut dict = TypstDict::default();
467
468            #[derive(Serialize)]
469            #[serde(rename_all = "camelCase")]
470            struct PreviewInputs {
471                pub version: u32,
472                pub theme: String,
473            }
474
475            dict.insert(
476                "x-preview".into(),
477                serde_json::to_string(&PreviewInputs {
478                    version: 1,
479                    theme: self.color_theme.clone().unwrap_or_default(),
480                })
481                .unwrap()
482                .into_value(),
483            );
484
485            Arc::new(LazyHash::new(dict))
486        };
487
488        self.validate()
489    }
490
491    /// Validates the configuration.
492    pub fn validate(&self) -> Result<()> {
493        self.entry_resolver.validate()?;
494
495        Ok(())
496    }
497
498    /// Gets the formatter configuration.
499    pub fn formatter(&self) -> FormatUserConfig {
500        let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
501        let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
502        let formatter_line_wrap = self.formatter_prose_wrap.unwrap_or(false);
503
504        FormatUserConfig {
505            config: match self.formatter_mode {
506                FormatterMode::Typstyle => {
507                    FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
508                        tab_spaces: formatter_indent_size,
509                        max_width: formatter_print_width,
510                        wrap_text: formatter_line_wrap,
511                        ..typstyle_core::Config::default()
512                    }))
513                }
514                FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
515                    max_line_length: formatter_print_width,
516                    indent_space: formatter_indent_size,
517                    line_wrap: formatter_line_wrap,
518                    ..typstfmt::Config::default()
519                })),
520                FormatterMode::Disable => FormatterConfig::Disable,
521            },
522            position_encoding: self.const_config.position_encoding,
523        }
524    }
525
526    /// Gets the preview configuration.
527    #[cfg(feature = "preview")]
528    pub fn preview(&self) -> PreviewConfig {
529        PreviewConfig {
530            enable_partial_rendering: self.preview.partial_rendering,
531            refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
532            invert_colors: serde_json::to_string(&self.preview.invert_colors)
533                .unwrap_or_else(|_| "never".to_string()),
534        }
535    }
536
537    /// Gets the export task configuration.
538    pub(crate) fn export_task(&self) -> ExportTask {
539        ExportTask {
540            when: self.export_pdf.clone(),
541            output: Some(self.output_path.clone()),
542            transform: vec![],
543        }
544    }
545
546    /// Gets the export configuration.
547    #[cfg(feature = "export")]
548    pub(crate) fn export(&self) -> ExportUserConfig {
549        let export = self.export_task();
550        ExportUserConfig {
551            export_target: self.export_target,
552            // todo: we only have `exportPdf` for now
553            // task: match self.export_target {
554            //     ExportTarget::Paged => ProjectTask::ExportPdf(ExportPdfTask {
555            //         export,
556            //         pdf_standards: vec![],
557            //         creation_timestamp: compile_config.determine_creation_timestamp(),
558            //     }),
559            //     ExportTarget::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
560            // },
561            task: ProjectTask::ExportPdf(ExportPdfTask {
562                export,
563                pdf_standards: self.pdf_standards().unwrap_or_default(),
564                creation_timestamp: self.creation_timestamp(),
565            }),
566            count_words: self.notify_status,
567            development: self.development,
568        }
569    }
570
571    /// Determines the font options.
572    pub fn font_opts(&self) -> CompileFontArgs {
573        let mut opts = self.font_opts.clone();
574
575        if let Some(system_fonts) = self.system_fonts.or_else(|| {
576            self.typst_extra_args
577                .as_ref()
578                .map(|x| !x.font.ignore_system_fonts)
579        }) {
580            opts.ignore_system_fonts = !system_fonts;
581        }
582
583        let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
584        let font_paths =
585            font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
586        if let Some(paths) = font_paths {
587            opts.font_paths.clone_from(paths);
588        }
589
590        let root = OnceLock::new();
591        for path in opts.font_paths.iter_mut() {
592            if path.is_relative() {
593                if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
594                    let p = std::mem::take(path);
595                    *path = root.join(p);
596                }
597            }
598        }
599
600        opts
601    }
602
603    /// Determines the package options.
604    pub fn package_opts(&self) -> CompilePackageArgs {
605        if let Some(extras) = &self.typst_extra_args {
606            return extras.package.clone();
607        }
608        CompilePackageArgs::default()
609    }
610
611    /// Determines the font resolver.
612    pub fn fonts(&self) -> Arc<FontResolverImpl> {
613        // todo: on font resolving failure, downgrade to a fake font book
614        let font = || {
615            let opts = self.font_opts();
616
617            log::info!("creating SharedFontResolver with {opts:?}");
618            Derived(
619                crate::project::LspUniverseBuilder::resolve_fonts(opts)
620                    .map(Arc::new)
621                    .expect("failed to create font book"),
622            )
623        };
624        self.fonts.get_or_init(font).clone().0
625    }
626
627    /// Determines the `sys.inputs` for the entry file.
628    pub fn inputs(&self) -> ImmutDict {
629        #[comemo::memoize]
630        fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
631            let mut dict = (**lhs).clone();
632            for (k, v) in rhs.iter() {
633                dict.insert(k.clone(), v.clone());
634            }
635
636            Arc::new(LazyHash::new(dict))
637        }
638
639        combine(self.user_inputs(), self.lsp_inputs.clone())
640    }
641
642    fn user_inputs(&self) -> ImmutDict {
643        static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
644
645        if let Some(extras) = &self.typst_extra_args {
646            return extras.inputs.clone();
647        }
648
649        EMPTY.clone()
650    }
651
652    /// Determines the typst features
653    pub fn typst_features(&self) -> Option<Features> {
654        let features = &self.typst_extra_args.as_ref()?.features;
655        Some(Features::from_iter(features.iter().map(|f| (*f).into())))
656    }
657
658    /// Determines the pdf standards.
659    pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
660        Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
661    }
662
663    /// Determines the creation timestamp.
664    pub fn creation_timestamp(&self) -> Option<i64> {
665        self.typst_extra_args.as_ref()?.creation_timestamp
666    }
667
668    /// Determines the certification path.
669    pub fn certification_path(&self) -> Option<ImmutPath> {
670        self.typst_extra_args.as_ref()?.cert.clone()
671    }
672
673    /// Applies the primary options related to compilation.
674    #[allow(clippy::type_complexity)]
675    pub fn primary_opts(
676        &self,
677    ) -> (
678        Option<bool>,
679        &Vec<PathBuf>,
680        Option<&CompileFontArgs>,
681        Option<i64>,
682        Option<Arc<Path>>,
683    ) {
684        (
685            self.system_fonts,
686            &self.font_paths,
687            self.typst_extra_args.as_ref().map(|e| &e.font),
688            self.creation_timestamp(),
689            self.entry_resolver
690                .root(self.entry_resolver.resolve_default().as_ref()),
691        )
692    }
693
694    #[cfg(not(feature = "system"))]
695    fn create_physical_access_model(
696        &self,
697        client: &TypedLspClient<ServerState>,
698    ) -> Arc<dyn LspAccessModel> {
699        self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
700    }
701
702    #[cfg(feature = "system")]
703    fn create_physical_access_model(
704        &self,
705        _client: &TypedLspClient<ServerState>,
706    ) -> Arc<dyn LspAccessModel> {
707        use reflexo_typst::vfs::system::SystemAccessModel;
708        Arc::new(SystemAccessModel {})
709    }
710
711    pub(crate) fn watch_access_model(
712        &self,
713        client: &TypedLspClient<ServerState>,
714    ) -> &Arc<WatchAccessModel> {
715        let client = client.clone();
716        &self
717            .watch_access_model
718            .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
719            .0
720    }
721
722    pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
723        let access_model = || {
724            log::info!(
725                "creating AccessModel with delegation={:?}",
726                self.delegate_fs_requests
727            );
728            if self.delegate_fs_requests {
729                Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
730            } else {
731                Derived(self.create_physical_access_model(client))
732            }
733        };
734        DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
735    }
736}
737
738/// Configuration set at initialization that won't change within a single
739/// session.
740#[derive(Debug, Clone)]
741pub struct ConstConfig {
742    /// Determined position encoding, either UTF-8 or UTF-16.
743    /// Defaults to UTF-16 if not specified.
744    pub position_encoding: PositionEncoding,
745    /// Allow dynamic registration of configuration changes.
746    pub cfg_change_registration: bool,
747    /// Allow notifying workspace/didRenameFiles
748    pub notify_will_rename_files: bool,
749    /// Allow dynamic registration of semantic tokens.
750    pub tokens_dynamic_registration: bool,
751    /// Allow overlapping tokens.
752    pub tokens_overlapping_token_support: bool,
753    /// Allow multiline tokens.
754    pub tokens_multiline_token_support: bool,
755    /// Allow line folding on documents.
756    pub doc_line_folding_only: bool,
757    /// Allow dynamic registration of document formatting.
758    pub doc_fmt_dynamic_registration: bool,
759    /// The locale of the editor.
760    pub locale: Option<String>,
761}
762
763impl Default for ConstConfig {
764    fn default() -> Self {
765        Self::from(&InitializeParams::default())
766    }
767}
768
769impl From<&InitializeParams> for ConstConfig {
770    fn from(params: &InitializeParams) -> Self {
771        const DEFAULT_ENCODING: &[PositionEncodingKind] = &[PositionEncodingKind::UTF16];
772
773        let position_encoding = {
774            let general = params.capabilities.general.as_ref();
775            let encodings = try_(|| Some(general?.position_encodings.as_ref()?.as_slice()));
776            let encodings = encodings.unwrap_or(DEFAULT_ENCODING);
777
778            if encodings.contains(&PositionEncodingKind::UTF8) {
779                PositionEncoding::Utf8
780            } else {
781                PositionEncoding::Utf16
782            }
783        };
784
785        let workspace = params.capabilities.workspace.as_ref();
786        let file_operations = try_(|| workspace?.file_operations.as_ref());
787        let doc = params.capabilities.text_document.as_ref();
788        let sema = try_(|| doc?.semantic_tokens.as_ref());
789        let fold = try_(|| doc?.folding_range.as_ref());
790        let format = try_(|| doc?.formatting.as_ref());
791
792        let locale = params
793            .initialization_options
794            .as_ref()
795            .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
796            .or(params.locale.as_deref());
797
798        Self {
799            position_encoding,
800            cfg_change_registration: try_or(|| workspace?.configuration, false),
801            notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
802            tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
803            tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
804            tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
805            doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
806            doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
807            locale: locale.map(ToOwned::to_owned),
808        }
809    }
810}
811
812impl From<&dapts::InitializeRequestArguments> for ConstConfig {
813    fn from(params: &dapts::InitializeRequestArguments) -> Self {
814        let locale = params.locale.as_deref();
815
816        Self {
817            locale: locale.map(ToOwned::to_owned),
818            ..Default::default()
819        }
820    }
821}
822
823/// Determines in what format paths are specified. The default is `path`, which
824/// is the native format.
825pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
826
827/// Configuration set at initialization that won't change within a single DAP
828/// session.
829#[derive(Debug, Clone)]
830pub struct ConstDapConfig {
831    /// The format of paths.
832    pub path_format: DapPathFormat,
833    /// Whether lines start at 1.
834    pub lines_start_at1: bool,
835    /// Whether columns start at 1.
836    pub columns_start_at1: bool,
837}
838
839impl Default for ConstDapConfig {
840    fn default() -> Self {
841        Self::from(&dapts::InitializeRequestArguments::default())
842    }
843}
844
845impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
846    fn from(params: &dapts::InitializeRequestArguments) -> Self {
847        Self {
848            path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
849            lines_start_at1: params.lines_start_at1.unwrap_or(true),
850            columns_start_at1: params.columns_start_at1.unwrap_or(true),
851        }
852    }
853}
854
855/// The mode of the formatter.
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
857#[serde(rename_all = "camelCase")]
858pub enum FormatterMode {
859    /// Disable the formatter.
860    #[default]
861    Disable,
862    /// Use `typstyle` formatter.
863    Typstyle,
864    /// Use `typstfmt` formatter.
865    Typstfmt,
866}
867
868/// The mode of semantic tokens.
869#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
870#[serde(rename_all = "camelCase")]
871pub enum SemanticTokensMode {
872    /// Disable the semantic tokens.
873    Disable,
874    /// Enable the semantic tokens.
875    #[default]
876    Enable,
877}
878
879/// The preview features.
880#[derive(Debug, Default, Clone, Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct PreviewFeat {
883    /// The browsing preview options.
884    #[serde(default, deserialize_with = "deserialize_null_default")]
885    pub browsing: BrowsingPreviewOpts,
886    /// The background preview options.
887    #[serde(default, deserialize_with = "deserialize_null_default")]
888    pub background: BackgroundPreviewOpts,
889    /// When to refresh the preview.
890    #[serde(default)]
891    pub refresh: Option<TaskWhen>,
892    /// Whether to enable partial rendering.
893    #[serde(default, deserialize_with = "deserialize_null_default")]
894    pub partial_rendering: bool,
895    /// Invert colors for the preview.
896    #[cfg(feature = "preview")]
897    #[serde(default, deserialize_with = "deserialize_null_default")]
898    pub invert_colors: PreviewInvertColors,
899}
900
901/// The lint features.
902#[derive(Debug, Default, Clone, Deserialize)]
903pub struct LintFeat {
904    /// Whether to enable linting.
905    pub enabled: Option<bool>,
906    /// When to trigger the lint checks.
907    pub when: Option<TaskWhen>,
908}
909
910impl LintFeat {
911    /// When to trigger the lint checks.
912    pub fn when(&self) -> &TaskWhen {
913        if matches!(self.enabled, Some(false) | None) {
914            return &TaskWhen::Never;
915        }
916
917        self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
918    }
919}
920/// The lint features.
921#[derive(Debug, Default, Clone, Deserialize)]
922#[serde(rename_all = "camelCase")]
923pub struct OnEnterFeat {
924    /// Whether to handle list.
925    #[serde(default, deserialize_with = "deserialize_null_default")]
926    pub handle_list: bool,
927}
928
929/// Options for browsing preview.
930#[derive(Debug, Default, Clone, Deserialize)]
931#[serde(rename_all = "camelCase")]
932pub struct BrowsingPreviewOpts {
933    /// The arguments for the `tinymist.startDefaultPreview` command.
934    pub args: Option<Vec<String>>,
935}
936
937/// Options for background preview.
938#[derive(Debug, Default, Clone, Deserialize)]
939#[serde(rename_all = "camelCase")]
940pub struct BackgroundPreviewOpts {
941    /// Whether to run the preview in the background.
942    #[serde(default, deserialize_with = "deserialize_null_default")]
943    pub enabled: bool,
944    /// The arguments for the background preview.
945    pub args: Option<Vec<String>>,
946}
947
948/// The extra typst arguments passed to the language server. You can pass any
949/// arguments as you like, and we will try to follow behaviors of the **same
950/// version** of typst-cli.
951#[derive(Debug, Clone, PartialEq, Default)]
952pub struct TypstExtraArgs {
953    /// The root directory for the compilation routine.
954    pub root_dir: Option<ImmutPath>,
955    /// The path to the entry.
956    pub entry: Option<ImmutPath>,
957    /// The additional input arguments to compile the entry file.
958    pub inputs: ImmutDict,
959    /// The additional font paths.
960    pub font: CompileFontArgs,
961    /// The package related arguments.
962    pub package: CompilePackageArgs,
963    /// One (or multiple comma-separated) PDF standards that Typst will enforce
964    /// conformance with.
965    pub features: Vec<Feature>,
966    /// One (or multiple comma-separated) PDF standards that Typst will enforce
967    /// conformance with.
968    pub pdf_standard: Vec<PdfStandard>,
969    /// The creation timestamp for various outputs (in seconds).
970    pub creation_timestamp: Option<i64>,
971    /// The path to the certification file.
972    pub cert: Option<ImmutPath>,
973}
974
975pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
976    SemanticTokensOptions {
977        legend: SemanticTokensLegend {
978            token_types: TokenType::iter()
979                .filter(|e| *e != TokenType::None)
980                .map(Into::into)
981                .collect(),
982            token_modifiers: Modifier::iter().map(Into::into).collect(),
983        },
984        full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
985        ..SemanticTokensOptions::default()
986    }
987}
988
989fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
990where
991    T: Default + Deserialize<'de>,
992    D: serde::Deserializer<'de>,
993{
994    let opt = Option::deserialize(deserializer)?;
995    Ok(opt.unwrap_or_default())
996}
997
998#[cfg(test)]
999mod tests {
1000    use super::*;
1001    use serde_json::json;
1002    #[cfg(feature = "preview")]
1003    use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1004
1005    fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1006        temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1007    }
1008
1009    fn good_config(config: &mut Config, update: &JsonValue) {
1010        update_config(config, update).expect("not good");
1011        assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1012    }
1013
1014    #[test]
1015    fn test_default_encoding() {
1016        let cc = ConstConfig::default();
1017        assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1018    }
1019
1020    #[test]
1021    fn test_config_update() {
1022        let mut config = Config::default();
1023
1024        let root_path = Path::new(if cfg!(windows) {
1025            "C:\\dummy-root"
1026        } else {
1027            "/dummy-root"
1028        });
1029
1030        let update = json!({
1031            "outputPath": "out",
1032            "exportPdf": "onSave",
1033            "rootPath": root_path,
1034            "semanticTokens": "enable",
1035            "formatterMode": "typstyle",
1036            "typstExtraArgs": ["--root", root_path]
1037        });
1038
1039        good_config(&mut config, &update);
1040
1041        // Nix specifies this environment variable when testing.
1042        let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1043        if has_source_date_epoch {
1044            let args = config.typst_extra_args.as_mut().unwrap();
1045            assert!(args.creation_timestamp.is_some());
1046            args.creation_timestamp = None;
1047        }
1048
1049        assert_eq!(config.output_path, PathPattern::new("out"));
1050        assert_eq!(config.export_pdf, TaskWhen::OnSave);
1051        assert_eq!(
1052            config.entry_resolver.root_path,
1053            Some(ImmutPath::from(root_path))
1054        );
1055        assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1056        assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1057        assert_eq!(
1058            config.typst_extra_args,
1059            Some(TypstExtraArgs {
1060                root_dir: Some(ImmutPath::from(root_path)),
1061                ..TypstExtraArgs::default()
1062            })
1063        );
1064    }
1065
1066    #[test]
1067    fn test_namespaced_config() {
1068        let mut config = Config::default();
1069
1070        // Emacs uses a shared configuration object for all language servers.
1071        let update = json!({
1072            "exportPdf": "onSave",
1073            "tinymist": {
1074                "exportPdf": "onType",
1075            }
1076        });
1077
1078        good_config(&mut config, &update);
1079
1080        assert_eq!(config.export_pdf, TaskWhen::OnType);
1081    }
1082
1083    #[test]
1084    fn test_compile_status() {
1085        let mut config = Config::default();
1086
1087        let update = json!({
1088            "compileStatus": "enable",
1089        });
1090        good_config(&mut config, &update);
1091        assert!(config.notify_status);
1092
1093        let update = json!({
1094            "compileStatus": "disable",
1095        });
1096        good_config(&mut config, &update);
1097        assert!(!config.notify_status);
1098    }
1099
1100    #[test]
1101    fn test_config_creation_timestamp() {
1102        type Timestamp = Option<i64>;
1103
1104        fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1105            let mut config = Config::default();
1106
1107            f(&mut config);
1108
1109            let args = config.typst_extra_args;
1110            args.and_then(|args| args.creation_timestamp)
1111        }
1112
1113        // assert!(timestamp(|_| {}).is_none());
1114        // assert!(timestamp(|config| {
1115        //     let update = json!({});
1116        //     good_config(&mut config, &update);
1117        // })
1118        // .is_none());
1119
1120        let args_timestamp = timestamp(|config| {
1121            let update = json!({
1122                "typstExtraArgs": ["--creation-timestamp", "1234"]
1123            });
1124            good_config(config, &update);
1125        });
1126        assert!(args_timestamp.is_some());
1127
1128        // todo: concurrent get/set env vars is unsafe
1129        //     std::env::set_var("SOURCE_DATE_EPOCH", "1234");
1130        //     let env_timestamp = timestamp(|config| {
1131        //         update_config(&mut config, &json!({})).unwrap();
1132        //     });
1133
1134        //     assert_eq!(args_timestamp, env_timestamp);
1135    }
1136
1137    #[test]
1138    fn test_empty_extra_args() {
1139        let mut config = Config::default();
1140        let update = json!({
1141            "typstExtraArgs": []
1142        });
1143
1144        good_config(&mut config, &update);
1145    }
1146
1147    #[test]
1148    fn test_null_args() {
1149        fn test_good_config(path: &str) -> Config {
1150            let mut obj = json!(null);
1151            let path = path.split('.').collect::<Vec<_>>();
1152            for p in path.iter().rev() {
1153                obj = json!({ *p: obj });
1154            }
1155
1156            let mut c = Config::default();
1157            good_config(&mut c, &obj);
1158            c
1159        }
1160
1161        test_good_config("root");
1162        test_good_config("rootPath");
1163        test_good_config("colorTheme");
1164        test_good_config("lint");
1165        test_good_config("customizedShowDocument");
1166        test_good_config("projectResolution");
1167        test_good_config("exportPdf");
1168        test_good_config("exportTarget");
1169        test_good_config("fontPaths");
1170        test_good_config("formatterMode");
1171        test_good_config("formatterPrintWidth");
1172        test_good_config("formatterIndentSize");
1173        test_good_config("formatterProseWrap");
1174        test_good_config("outputPath");
1175        test_good_config("semanticTokens");
1176        test_good_config("delegateFsRequests");
1177        test_good_config("supportHtmlInMarkdown");
1178        test_good_config("supportExtendedCodeAction");
1179        test_good_config("development");
1180        test_good_config("systemFonts");
1181
1182        test_good_config("completion");
1183        test_good_config("completion.triggerSuggest");
1184        test_good_config("completion.triggerParameterHints");
1185        test_good_config("completion.triggerSuggestAndParameterHints");
1186        test_good_config("completion.triggerOnSnippetPlaceholders");
1187        test_good_config("completion.symbol");
1188        test_good_config("completion.postfix");
1189        test_good_config("completion.postfixUfcs");
1190        test_good_config("completion.postfixUfcsLeft");
1191        test_good_config("completion.postfixUfcsRight");
1192        test_good_config("completion.postfixSnippets");
1193
1194        test_good_config("lint");
1195        test_good_config("lint.enabled");
1196        test_good_config("lint.when");
1197
1198        test_good_config("preview");
1199        test_good_config("preview.browsing");
1200        test_good_config("preview.browsing.args");
1201        test_good_config("preview.background");
1202        test_good_config("preview.background.enabled");
1203        test_good_config("preview.background.args");
1204        test_good_config("preview.refresh");
1205        test_good_config("preview.partialRendering");
1206        #[cfg(feature = "preview")]
1207        let c = test_good_config("preview.invertColors");
1208        #[cfg(feature = "preview")]
1209        assert_eq!(
1210            c.preview.invert_colors,
1211            PreviewInvertColors::Enum(PreviewInvertColor::Never)
1212        );
1213    }
1214
1215    #[test]
1216    fn test_font_opts() {
1217        fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1218            let mut config = Config::default();
1219            if let Some(update) = update {
1220                good_config(&mut config, update);
1221            }
1222
1223            config.font_opts()
1224        }
1225
1226        let font_opts = opts(None);
1227        assert!(!font_opts.ignore_system_fonts);
1228
1229        let font_opts = opts(Some(&json!({})));
1230        assert!(!font_opts.ignore_system_fonts);
1231
1232        let font_opts = opts(Some(&json!({
1233            "typstExtraArgs": []
1234        })));
1235        assert!(!font_opts.ignore_system_fonts);
1236
1237        let font_opts = opts(Some(&json!({
1238            "systemFonts": false,
1239        })));
1240        assert!(font_opts.ignore_system_fonts);
1241
1242        let font_opts = opts(Some(&json!({
1243            "typstExtraArgs": ["--ignore-system-fonts"]
1244        })));
1245        assert!(font_opts.ignore_system_fonts);
1246
1247        let font_opts = opts(Some(&json!({
1248            "systemFonts": true,
1249            "typstExtraArgs": ["--ignore-system-fonts"]
1250        })));
1251        assert!(!font_opts.ignore_system_fonts);
1252    }
1253
1254    #[test]
1255    fn test_preview_opts() {
1256        fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1257            let mut config = Config::default();
1258            if let Some(update) = update {
1259                good_config(&mut config, update);
1260            }
1261
1262            config.preview
1263        }
1264
1265        let preview = opts(Some(&json!({
1266            "preview": {
1267            }
1268        })));
1269        assert_eq!(preview.refresh, None);
1270
1271        let preview = opts(Some(&json!({
1272            "preview": {
1273                "refresh":"onType"
1274            }
1275        })));
1276        assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1277
1278        let preview = opts(Some(&json!({
1279            "preview": {
1280                "refresh":"onSave"
1281            }
1282        })));
1283        assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1284    }
1285
1286    #[test]
1287    fn test_reject_abnormal_root() {
1288        let mut config = Config::default();
1289        let update = json!({
1290            "rootPath": ".",
1291        });
1292
1293        let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1294        assert!(err.contains("absolute path"), "unexpected error: {err}");
1295    }
1296
1297    #[test]
1298    fn test_reject_abnormal_root2() {
1299        let mut config = Config::default();
1300        let update = json!({
1301            "typstExtraArgs": ["--root", "."]
1302        });
1303
1304        let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1305        assert!(err.contains("absolute path"), "unexpected error: {err}");
1306    }
1307
1308    #[test]
1309    fn test_entry_by_extra_args() {
1310        let simple_config = {
1311            let mut config = Config::default();
1312            let update = json!({
1313                "typstExtraArgs": ["main.typ"]
1314            });
1315
1316            // It should be able to resolve the entry file from the extra arguments.
1317            update_config(&mut config, &update).expect("updated");
1318            // Passing it twice doesn't affect the result.
1319            update_config(&mut config, &update).expect("updated");
1320            config
1321        };
1322        {
1323            let mut config = Config::default();
1324            let update = json!({
1325                "typstExtraArgs": ["main.typ", "main.typ"]
1326            });
1327            update_config(&mut config, &update).unwrap();
1328            let warns = format!("{:?}", config.warnings);
1329            assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1330            assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1331        }
1332        {
1333            let mut config = Config::default();
1334            let update = json!({
1335                "typstExtraArgs": ["main2.typ"],
1336                "tinymist": {
1337                    "typstExtraArgs": ["main.typ"]
1338                }
1339            });
1340
1341            // It should be able to resolve the entry file from the extra arguments.
1342            update_config(&mut config, &update).expect("updated");
1343            // Passing it twice doesn't affect the result.
1344            update_config(&mut config, &update).expect("updated");
1345
1346            assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1347        }
1348    }
1349
1350    #[test]
1351    fn test_default_formatting_config() {
1352        let config = Config::default().formatter();
1353        assert!(matches!(config.config, FormatterConfig::Disable));
1354        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1355    }
1356
1357    #[test]
1358    fn test_typstyle_formatting_config() {
1359        let config = Config {
1360            formatter_mode: FormatterMode::Typstyle,
1361            ..Config::default()
1362        };
1363        let config = config.formatter();
1364        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1365
1366        let typstyle_config = match config.config {
1367            FormatterConfig::Typstyle(e) => e,
1368            _ => panic!("unexpected configuration of formatter"),
1369        };
1370
1371        assert_eq!(typstyle_config.max_width, 120);
1372    }
1373
1374    #[test]
1375    fn test_typstyle_formatting_config_set_width() {
1376        let config = Config {
1377            formatter_mode: FormatterMode::Typstyle,
1378            formatter_print_width: Some(240),
1379            ..Config::default()
1380        };
1381        let config = config.formatter();
1382        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1383
1384        let typstyle_config = match config.config {
1385            FormatterConfig::Typstyle(e) => e,
1386            _ => panic!("unexpected configuration of formatter"),
1387        };
1388
1389        assert_eq!(typstyle_config.max_width, 240);
1390    }
1391
1392    #[test]
1393    fn test_typstyle_formatting_config_set_tab_spaces() {
1394        let config = Config {
1395            formatter_mode: FormatterMode::Typstyle,
1396            formatter_indent_size: Some(8),
1397            ..Config::default()
1398        };
1399        let config = config.formatter();
1400        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1401
1402        let typstyle_config = match config.config {
1403            FormatterConfig::Typstyle(e) => e,
1404            _ => panic!("unexpected configuration of formatter"),
1405        };
1406
1407        assert_eq!(typstyle_config.tab_spaces, 8);
1408    }
1409
1410    #[test]
1411    #[cfg(feature = "preview")]
1412    fn test_default_preview_config() {
1413        let config = Config::default().preview();
1414        assert!(!config.enable_partial_rendering);
1415        assert_eq!(config.refresh_style, TaskWhen::OnType);
1416        assert_eq!(config.invert_colors, "\"never\"");
1417    }
1418
1419    #[test]
1420    #[cfg(feature = "preview")]
1421    fn test_preview_config() {
1422        let config = Config {
1423            preview: PreviewFeat {
1424                partial_rendering: true,
1425                refresh: Some(TaskWhen::OnSave),
1426                invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1427                ..PreviewFeat::default()
1428            },
1429            ..Config::default()
1430        }
1431        .preview();
1432
1433        assert!(config.enable_partial_rendering);
1434        assert_eq!(config.refresh_style, TaskWhen::OnSave);
1435        assert_eq!(config.invert_colors, "\"auto\"");
1436    }
1437
1438    #[test]
1439    fn test_default_lsp_config_initialize() {
1440        let (_conf, err) =
1441            Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1442        assert!(err.is_none());
1443    }
1444
1445    #[test]
1446    fn test_default_dap_config_initialize() {
1447        let (_conf, err) = Config::extract_dap_params(
1448            dapts::InitializeRequestArguments::default(),
1449            CompileFontArgs::default(),
1450        );
1451        assert!(err.is_none());
1452    }
1453
1454    #[test]
1455    fn test_config_package_path_from_env() {
1456        let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
1457
1458        temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
1459            let (conf, err) =
1460                Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1461            assert!(err.is_none());
1462            let applied_cache_path = conf
1463                .typst_extra_args
1464                .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
1465            assert!(applied_cache_path);
1466        });
1467    }
1468
1469    #[test]
1470    #[cfg(feature = "preview")]
1471    fn test_invert_colors_validation() {
1472        fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
1473            Ok(serde_json::from_str(s)?)
1474        }
1475
1476        assert_eq!(
1477            test(r#""never""#).unwrap(),
1478            PreviewInvertColors::Enum(PreviewInvertColor::Never)
1479        );
1480        assert_eq!(
1481            test(r#""auto""#).unwrap(),
1482            PreviewInvertColors::Enum(PreviewInvertColor::Auto)
1483        );
1484        assert_eq!(
1485            test(r#""always""#).unwrap(),
1486            PreviewInvertColors::Enum(PreviewInvertColor::Always)
1487        );
1488        assert!(test(r#""e""#).is_err());
1489
1490        assert_eq!(
1491            test(r#"{"rest": "never"}"#).unwrap(),
1492            PreviewInvertColors::Object(PreviewInvertColorObject {
1493                image: PreviewInvertColor::Never,
1494                rest: PreviewInvertColor::Never,
1495            })
1496        );
1497        assert_eq!(
1498            test(r#"{"image": "always"}"#).unwrap(),
1499            PreviewInvertColors::Object(PreviewInvertColorObject {
1500                image: PreviewInvertColor::Always,
1501                rest: PreviewInvertColor::Never,
1502            })
1503        );
1504        assert_eq!(
1505            test(r#"{}"#).unwrap(),
1506            PreviewInvertColors::Object(PreviewInvertColorObject {
1507                image: PreviewInvertColor::Never,
1508                rest: PreviewInvertColor::Never,
1509            })
1510        );
1511        assert_eq!(
1512            test(r#"{"unknown": "ovo"}"#).unwrap(),
1513            PreviewInvertColors::Object(PreviewInvertColorObject {
1514                image: PreviewInvertColor::Never,
1515                rest: PreviewInvertColor::Never,
1516            })
1517        );
1518        assert!(test(r#"{"image": "e"}"#).is_err());
1519    }
1520}