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;
25use typst_shim::SYNTAX_ONLY;
26
27use super::*;
28use crate::input::WatchAccessModel;
29use crate::project::{
30    EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen,
31};
32use crate::world::font::FontResolverImpl;
33
34#[cfg(feature = "export")]
35use task::ExportUserConfig;
36#[cfg(feature = "preview")]
37use tinymist_preview::{PreviewConfig, PreviewInvertColors};
38
39#[cfg(feature = "export")]
40use crate::project::{ExportPdfTask, ProjectTask};
41
42// region Configuration Items
43const CONFIG_ITEMS: &[&str] = &[
44    "tinymist",
45    "colorTheme",
46    "compileStatus",
47    "lint",
48    "completion",
49    "development",
50    "exportPdf",
51    "exportTarget",
52    "fontPaths",
53    "formatterMode",
54    "formatterPrintWidth",
55    "formatterIndentSize",
56    "formatterProseWrap",
57    "hoverPeriscope",
58    "onEnter",
59    "outputPath",
60    "syntaxOnly",
61    "preview",
62    "projectResolution",
63    "rootPath",
64    "semanticTokens",
65    "systemFonts",
66    "typstExtraArgs",
67];
68// endregion Configuration Items
69
70/// The user configuration read from the editor.
71///
72/// Note: `Config::default` is intentionally to be "pure" and not to be
73/// affected by system environment variables.
74/// To get the configuration with system defaults, use [`Config::new`] instead.
75#[derive(Debug, Default, Clone)]
76pub struct Config {
77    /// Constant configuration during session.
78    pub const_config: ConstConfig,
79    /// Constant DAP-specific configuration during session.
80    pub const_dap_config: ConstDapConfig,
81
82    /// Whether to delegate file system accesses to the client.
83    pub delegate_fs_requests: bool,
84    /// Whether to send show document requests with customized notification.
85    pub customized_show_document: bool,
86    /// Whether the configuration can have a default entry path.
87    pub has_default_entry_path: bool,
88    /// Whether to notify the status to the editor.
89    pub notify_status: bool,
90    /// Whether to remove HTML from markup content in responses.
91    pub support_html_in_markdown: bool,
92    /// Whether to utilize the extended `tinymist.resolveCodeAction` at client
93    /// side.
94    pub extended_code_action: bool,
95    /// Whether to run the server in development mode.
96    pub development: bool,
97    /// Whether to run the server in syntax-only mode.
98    pub syntax_only: bool,
99
100    /// The preferred color theme for rendering.
101    pub color_theme: Option<String>,
102    /// The entry resolver.
103    pub entry_resolver: EntryResolver,
104    /// The `sys.inputs` passed to the typst compiler.
105    pub lsp_inputs: ImmutDict,
106    /// The arguments about periscope rendering in hover window.
107    pub periscope_args: Option<PeriscopeArgs>,
108    /// The extra typst arguments passed to the language server.
109    pub typst_extra_args: Option<TypstExtraArgs>,
110    /// The dynamic configuration for semantic tokens.
111    pub semantic_tokens: SemanticTokensMode,
112
113    /// Tinymist's completion features.
114    pub completion: CompletionFeat,
115    /// Tinymist's preview features.
116    pub preview: PreviewFeat,
117    /// Tinymist's lint features.
118    pub lint: LintFeat,
119    /// Tinymist's on-enter features.
120    pub on_enter: OnEnterFeat,
121
122    /// Specifies the cli font options
123    pub font_opts: CompileFontArgs,
124    /// Specifies the font paths
125    pub font_paths: Vec<PathBuf>,
126    /// Computed fonts based on configuration.
127    pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
128    /// Whether to use system fonts.
129    pub system_fonts: Option<bool>,
130
131    /// Computed watch access model based on configuration.
132    pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
133    /// Computed access model based on configuration.
134    pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
135
136    /// Tinymist's default export target.
137    pub export_target: ExportTarget,
138    /// The mode of PDF export.
139    pub export_pdf: TaskWhen,
140    /// The output directory for PDF export.
141    pub output_path: PathPattern,
142
143    /// Dynamic configuration for the experimental formatter.
144    pub formatter_mode: FormatterMode,
145    /// Sets the print width for the formatter, which is a **soft limit** of
146    /// characters per line. See [the definition of *Print Width*](https://prettier.io/docs/en/options.html#print-width).
147    pub formatter_print_width: Option<u32>,
148    /// Sets the indent size (using space) for the formatter.
149    pub formatter_indent_size: Option<u32>,
150    /// Sets the hard line wrapping mode for the formatter.
151    pub formatter_prose_wrap: Option<bool>,
152    /// The warnings during configuration update.
153    pub warnings: Vec<CowStr>,
154}
155
156impl Config {
157    /// Creates a new configuration with system defaults.
158    pub fn new(
159        const_config: ConstConfig,
160        roots: Vec<ImmutPath>,
161        font_opts: CompileFontArgs,
162    ) -> Self {
163        let mut config = Self {
164            const_config,
165            const_dap_config: ConstDapConfig::default(),
166            entry_resolver: EntryResolver {
167                roots,
168                ..EntryResolver::default()
169            },
170            font_opts,
171            ..Self::default()
172        };
173        config
174            .update_by_map(&Map::default())
175            .log_error("failed to assign Config defaults");
176        config
177    }
178
179    /// Creates a new configuration from the LSP initialization parameters.
180    ///
181    /// The function has side effects:
182    /// - Getting environment variables.
183    /// - Setting the locale.
184    pub fn extract_lsp_params(
185        params: InitializeParams,
186        font_args: CompileFontArgs,
187    ) -> (Self, Option<ResponseError>) {
188        // Initialize configurations
189        let roots = match params.workspace_folders.as_ref() {
190            Some(roots) => roots
191                .iter()
192                .map(|root| ImmutPath::from(url_to_path(&root.uri)))
193                .collect(),
194            #[allow(deprecated)] // `params.root_path` is marked as deprecated
195            None => params
196                .root_uri
197                .as_ref()
198                .map(|uri| ImmutPath::from(url_to_path(uri)))
199                .or_else(|| Some(Path::new(&params.root_path.as_ref()?).into()))
200                .into_iter()
201                .collect(),
202        };
203        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
204
205        // Sets locale as soon as possible
206        if let Some(locale) = config.const_config.locale.as_ref() {
207            tinymist_l10n::set_locale(locale);
208        }
209        config.configure_syntax_only();
210
211        let err = params
212            .initialization_options
213            .and_then(|init| config.update(&init).map_err(invalid_params).err());
214
215        (config, err)
216    }
217
218    /// Creates a new configuration from the DAP initialization parameters.
219    ///
220    /// The function has side effects:
221    /// - Getting environment variables.
222    /// - Setting the locale.
223    pub fn extract_dap_params(
224        params: dapts::InitializeRequestArguments,
225        font_args: CompileFontArgs,
226    ) -> (Self, Option<ResponseError>) {
227        // This is reliable in DAP context.
228        let cwd = std::env::current_dir()
229            .expect("failed to get current directory")
230            .into();
231
232        // Initialize configurations
233        let roots = vec![cwd];
234        let mut config = Config::new(ConstConfig::from(&params), roots, font_args);
235        config.const_dap_config = ConstDapConfig::from(&params);
236
237        // Sets locale as soon as possible
238        if let Some(locale) = config.const_config.locale.as_ref() {
239            tinymist_l10n::set_locale(locale);
240        }
241
242        (config, None)
243    }
244
245    /// Gets configuration descriptors to request configuration sections from
246    /// the client.
247    pub fn get_items() -> Vec<ConfigurationItem> {
248        CONFIG_ITEMS
249            .iter()
250            .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
251            .map(|section| ConfigurationItem {
252                section: Some(section),
253                ..ConfigurationItem::default()
254            })
255            .collect()
256    }
257
258    /// Converts config values to a map object.
259    pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
260        let unpaired_values = values
261            .into_iter()
262            .tuples()
263            .map(|(a, b)| if !a.is_null() { a } else { b });
264
265        CONFIG_ITEMS
266            .iter()
267            .map(|&item| item.to_owned())
268            .zip(unpaired_values)
269            .collect()
270    }
271
272    /// Updates (and validates) the configuration by a JSON object.
273    ///
274    /// The config may be broken if the update is invalid. Please clone the
275    /// configuration before updating and revert if the update fails.
276    pub fn update(&mut self, update: &JsonValue) -> Result<()> {
277        if let JsonValue::Object(update) = update {
278            self.update_by_map(update)?;
279
280            // Configurations in the tinymist namespace take precedence.
281            if let Some(namespaced) = update.get("tinymist").and_then(JsonValue::as_object) {
282                self.update_by_map(namespaced)?;
283            }
284
285            Ok(())
286        } else {
287            tinymist_l10n::bail!(
288                "tinymist.config.invalidObject",
289                "invalid configuration object: {object}",
290                object = update.debug_l10n(),
291            )
292        }
293    }
294
295    /// Updates (and validates) the configuration by a map object.
296    ///
297    /// The config may be broken if the update is invalid. Please clone the
298    /// configuration before updating and revert if the update fails.
299    pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> Result<()> {
300        log::info!(
301            "ServerState: config update_by_map {}",
302            serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
303        );
304
305        self.warnings.clear();
306
307        macro_rules! try_deserialize {
308            ($ty:ty, $key:expr) => {
309                update.get($key).and_then(|v| {
310                    <$ty>::deserialize(v)
311                        .inspect_err(|err| {
312                            // Only ignore null returns. Some editors may send null values when
313                            // the configuration is not set, e.g. Zed.
314                            if v.is_null() {
315                                return;
316                            }
317
318                            self.warnings.push(tinymist_l10n::t!(
319                                "tinymist.config.deserializeError",
320                                "failed to deserialize \"{key}\": {err}",
321                                key = $key.debug_l10n(),
322                                err = err.debug_l10n(),
323                            ));
324                        })
325                        .ok()
326                })
327            };
328        }
329
330        macro_rules! assign_config {
331            ($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
332                let v = try_deserialize!($ty, $bind);
333                self.$($field_path).+ = v.unwrap_or_default();
334            };
335            ($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
336                let v = try_deserialize!($ty, $bind);
337                self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
338            };
339        }
340
341        assign_config!(color_theme := "colorTheme"?: Option<String>);
342        assign_config!(lint := "lint"?: LintFeat);
343        assign_config!(completion := "completion"?: CompletionFeat);
344        assign_config!(on_enter := "onEnter"?: OnEnterFeat);
345        assign_config!(completion.trigger_suggest := "triggerSuggest"?: bool);
346        assign_config!(completion.trigger_parameter_hints := "triggerParameterHints"?: bool);
347        assign_config!(completion.trigger_suggest_and_parameter_hints := "triggerSuggestAndParameterHints"?: bool);
348        assign_config!(customized_show_document := "customizedShowDocument"?: bool);
349        assign_config!(entry_resolver.project_resolution := "projectResolution"?: ProjectResolutionKind);
350        assign_config!(export_pdf := "exportPdf"?: TaskWhen);
351        assign_config!(export_target := "exportTarget"?: ExportTarget);
352        assign_config!(font_paths := "fontPaths"?: Vec<_>);
353        assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
354        assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
355        assign_config!(formatter_indent_size := "formatterIndentSize"?: Option<u32>);
356        assign_config!(formatter_prose_wrap := "formatterProseWrap"?: Option<bool>);
357        assign_config!(output_path := "outputPath"?: PathPattern);
358        assign_config!(preview := "preview"?: PreviewFeat);
359        assign_config!(lint := "lint"?: LintFeat);
360        assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
361        assign_config!(delegate_fs_requests := "delegateFsRequests"?: bool);
362        assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
363        assign_config!(extended_code_action := "supportExtendedCodeAction"?: bool);
364        assign_config!(development := "development"?: bool);
365        assign_config!(system_fonts := "systemFonts"?: Option<bool>);
366
367        self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
368            Some("enable") => true,
369            Some("disable") | None => false,
370            Some(value) => {
371                self.warnings.push(tinymist_l10n::t!(
372                    "tinymist.config.badCompileStatus",
373                    "compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
374                    value = value.debug_l10n(),
375                ));
376
377                false
378            }
379        };
380        self.syntax_only = match try_(|| update.get("syntaxOnly")?.as_str()) {
381            Some("onPowerSaving") => tinymist_std::battery::is_power_saving(),
382            Some("enable") => true,
383            Some("disable" | "auto") | None => false,
384            Some(value) => {
385                self.warnings.push(tinymist_l10n::t!(
386                    "tinymist.config.badSyntaxOnly",
387                    "syntaxOnly must be either `\"enable\"`, `\"disable\", `\"onPowerSaving\"`, or `\"auto\"`, got {value}",
388                    value = value.debug_l10n(),
389                ));
390
391                false
392            }
393        };
394
395        // periscope_args
396        self.periscope_args = match update.get("hoverPeriscope") {
397            Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()),
398            Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None,
399            Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
400                Ok(args) => Some(args),
401                Err(err) => {
402                    self.warnings.push(tinymist_l10n::t!(
403                        "tinymist.config.badHoverPeriscope",
404                        "failed to parse hoverPeriscope: {err}",
405                        err = err.debug_l10n(),
406                    ));
407                    None
408                }
409            },
410        };
411        if let Some(args) = self.periscope_args.as_mut() {
412            if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") {
413                "always".clone_into(&mut args.invert_color);
414            }
415        }
416
417        fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> CowStr {
418            log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
419            tinymist_l10n::t!(
420                "tinymist.config.badTypstExtraArgs",
421                "failed to parse typstExtraArgs: {err}, args: {args}",
422                err = err.debug_l10n(),
423                args = args.debug_l10n(),
424            )
425        }
426
427        {
428            let raw_args = || update.get("typstExtraArgs");
429            let typst_args: Vec<String> = match raw_args().cloned().map(serde_json::from_value) {
430                Some(Ok(args)) => args,
431                Some(Err(err)) => {
432                    self.warnings.push(invalid_extra_args(&raw_args(), err));
433                    None
434                }
435                // Even if the list is none, it should be parsed since we have env vars to
436                // retrieve.
437                None => None,
438            }
439            .unwrap_or_default();
440            let empty_typst_args = typst_args.is_empty();
441
442            let args = match CompileOnceArgs::try_parse_from(
443                Some("typst-cli".to_owned()).into_iter().chain(typst_args),
444            ) {
445                Ok(args) => args,
446                Err(err) => {
447                    self.warnings.push(invalid_extra_args(&raw_args(), err));
448
449                    if empty_typst_args {
450                        CompileOnceArgs::default()
451                    } else {
452                        // Still try to parse the arguments to get the environment variables.
453                        CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
454                            .inspect_err(|err| {
455                                log::error!("failed to make default typstExtraArgs: {err}");
456                            })
457                            .unwrap_or_default()
458                    }
459                }
460            };
461
462            // todo: the command.root may be not absolute
463            self.typst_extra_args = Some(TypstExtraArgs {
464                inputs: args.resolve_inputs().unwrap_or_default(),
465                entry: args.input.map(|e| Path::new(&e).into()),
466                root_dir: args.root.as_ref().map(|r| r.as_path().into()),
467                font: args.font,
468                package: args.package,
469                pdf_standard: args.pdf.standard,
470                no_pdf_tags: args.pdf.no_tags,
471                ppi: args.png.ppi,
472                features: args.features,
473                creation_timestamp: args.creation_timestamp,
474                cert: args.cert.as_deref().map(From::from),
475            });
476        }
477
478        self.entry_resolver.root_path =
479            try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
480                self.typst_extra_args
481                    .as_ref()
482                    .and_then(|e| e.root_dir.clone())
483            });
484        self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
485        self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
486        self.lsp_inputs = {
487            let mut dict = TypstDict::default();
488
489            #[derive(Serialize)]
490            #[serde(rename_all = "camelCase")]
491            struct PreviewInputs {
492                pub version: u32,
493                pub theme: String,
494            }
495
496            dict.insert(
497                "x-preview".into(),
498                serde_json::to_string(&PreviewInputs {
499                    version: 1,
500                    theme: self.color_theme.clone().unwrap_or_default(),
501                })
502                .unwrap()
503                .into_value(),
504            );
505
506            Arc::new(LazyHash::new(dict))
507        };
508
509        self.validate()
510    }
511
512    /// Validates the configuration.
513    pub fn validate(&self) -> Result<()> {
514        self.entry_resolver.validate()?;
515
516        Ok(())
517    }
518
519    /// Configures the syntax-only mode.
520    pub fn configure_syntax_only(&self) {
521        if self.syntax_only {
522            log::info!("Server: running lsp in syntax-only mode, some features may be disabled");
523            SYNTAX_ONLY.store(true, std::sync::atomic::Ordering::SeqCst);
524        } else {
525            log::info!("Server: running lsp in full mode");
526            SYNTAX_ONLY.store(false, std::sync::atomic::Ordering::SeqCst);
527        }
528    }
529
530    /// Gets the formatter configuration.
531    pub fn formatter(&self) -> FormatUserConfig {
532        let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
533        let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
534        let formatter_line_wrap = self.formatter_prose_wrap.unwrap_or(false);
535
536        FormatUserConfig {
537            config: match self.formatter_mode {
538                FormatterMode::Typstyle => {
539                    FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
540                        tab_spaces: formatter_indent_size,
541                        max_width: formatter_print_width,
542                        wrap_text: formatter_line_wrap,
543                        ..typstyle_core::Config::default()
544                    }))
545                }
546                FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
547                    max_line_length: formatter_print_width,
548                    indent_space: formatter_indent_size,
549                    line_wrap: formatter_line_wrap,
550                    ..typstfmt::Config::default()
551                })),
552                FormatterMode::Disable => FormatterConfig::Disable,
553            },
554            position_encoding: self.const_config.position_encoding,
555        }
556    }
557
558    /// Gets the preview configuration.
559    #[cfg(feature = "preview")]
560    pub fn preview(&self) -> PreviewConfig {
561        PreviewConfig {
562            enable_partial_rendering: self.preview.partial_rendering,
563            refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
564            invert_colors: serde_json::to_string(&self.preview.invert_colors)
565                .unwrap_or_else(|_| "never".to_string()),
566        }
567    }
568
569    /// Gets the export task configuration.
570    pub(crate) fn export_task(&self) -> ExportTask {
571        ExportTask {
572            when: self.export_pdf.clone(),
573            output: Some(self.output_path.clone()),
574            transform: vec![],
575        }
576    }
577
578    /// Gets the export configuration.
579    #[cfg(feature = "export")]
580    pub(crate) fn export(&self) -> ExportUserConfig {
581        let export = self.export_task();
582        ExportUserConfig {
583            export_target: self.export_target,
584            // todo: we only have `exportPdf` for now
585            // task: match self.export_target {
586            //     ExportTarget::Paged => ProjectTask::ExportPdf(ExportPdfTask {
587            //         export,
588            //         pdf_standards: vec![],
589            //         creation_timestamp: compile_config.determine_creation_timestamp(),
590            //     }),
591            //     ExportTarget::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
592            // },
593            task: ProjectTask::ExportPdf(ExportPdfTask {
594                export,
595                pages: None, // todo: set pages
596                pdf_standards: self.pdf_standards().unwrap_or_default(),
597                no_pdf_tags: self.no_pdf_tags(),
598                creation_timestamp: self.creation_timestamp(),
599            }),
600            count_words: self.notify_status,
601            development: self.development,
602        }
603    }
604
605    /// Determines the font options.
606    pub fn font_opts(&self) -> CompileFontArgs {
607        let mut opts = self.font_opts.clone();
608
609        if let Some(system_fonts) = self.system_fonts.or_else(|| {
610            self.typst_extra_args
611                .as_ref()
612                .map(|x| !x.font.ignore_system_fonts)
613        }) {
614            opts.ignore_system_fonts = !system_fonts;
615        }
616
617        let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
618        let font_paths =
619            font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
620        if let Some(paths) = font_paths {
621            opts.font_paths.clone_from(paths);
622        }
623
624        let root = OnceLock::new();
625        for path in opts.font_paths.iter_mut() {
626            if path.is_relative() {
627                if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
628                    let p = std::mem::take(path);
629                    *path = root.join(p);
630                }
631            }
632        }
633
634        opts
635    }
636
637    /// Determines the package options.
638    pub fn package_opts(&self) -> CompilePackageArgs {
639        if let Some(extras) = &self.typst_extra_args {
640            return extras.package.clone();
641        }
642        CompilePackageArgs::default()
643    }
644
645    /// Determines the font resolver.
646    pub fn fonts(&self) -> Arc<FontResolverImpl> {
647        // todo: on font resolving failure, downgrade to a fake font book
648        let font = || {
649            let opts = self.font_opts();
650
651            log::info!("creating SharedFontResolver with {opts:?}");
652            Derived(
653                crate::project::LspUniverseBuilder::resolve_fonts(opts)
654                    .map(Arc::new)
655                    .expect("failed to create font book"),
656            )
657        };
658        self.fonts.get_or_init(font).clone().0
659    }
660
661    /// Determines the `sys.inputs` for the entry file.
662    pub fn inputs(&self) -> ImmutDict {
663        #[comemo::memoize]
664        fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
665            let mut dict = (**lhs).clone();
666            for (k, v) in rhs.iter() {
667                dict.insert(k.clone(), v.clone());
668            }
669
670            Arc::new(LazyHash::new(dict))
671        }
672
673        combine(self.user_inputs(), self.lsp_inputs.clone())
674    }
675
676    fn user_inputs(&self) -> ImmutDict {
677        static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
678
679        if let Some(extras) = &self.typst_extra_args {
680            return extras.inputs.clone();
681        }
682
683        EMPTY.clone()
684    }
685
686    /// Determines the typst features
687    pub fn typst_features(&self) -> Option<Features> {
688        let features = &self.typst_extra_args.as_ref()?.features;
689        Some(Features::from_iter(features.iter().map(|f| (*f).into())))
690    }
691
692    /// Determines the pdf standards.
693    pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
694        Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
695    }
696
697    /// Determines the no pdf tags.
698    pub fn no_pdf_tags(&self) -> bool {
699        self.typst_extra_args
700            .as_ref()
701            .is_some_and(|x| x.no_pdf_tags)
702    }
703
704    /// Determines the ppi.
705    pub fn ppi(&self) -> Option<f32> {
706        Some(self.typst_extra_args.as_ref()?.ppi)
707    }
708
709    /// Determines the creation timestamp.
710    pub fn creation_timestamp(&self) -> Option<i64> {
711        self.typst_extra_args.as_ref()?.creation_timestamp
712    }
713
714    /// Determines the certification path.
715    pub fn certification_path(&self) -> Option<ImmutPath> {
716        self.typst_extra_args.as_ref()?.cert.clone()
717    }
718
719    /// Applies the primary options related to compilation.
720    #[allow(clippy::type_complexity)]
721    pub fn primary_opts(
722        &self,
723    ) -> (
724        bool,
725        ImmutDict,
726        ExportTarget,
727        Option<Vec<typst::Feature>>,
728        Option<ImmutPath>,
729        CompilePackageArgs,
730        Option<bool>,
731        CompileFontArgs,
732        Option<i64>,
733        Option<Arc<Path>>,
734    ) {
735        (
736            // server
737            self.syntax_only,
738            // typst library
739            self.user_inputs(),
740            self.export_target,
741            self.typst_features().map(|feat| {
742                let mut features = vec![];
743                if feat.is_enabled(typst::Feature::Html) {
744                    features.push(typst::Feature::Html);
745                }
746                if feat.is_enabled(typst::Feature::A11yExtras) {
747                    features.push(typst::Feature::A11yExtras);
748                }
749
750                features
751            }),
752            // typst package
753            self.certification_path(),
754            self.package_opts(),
755            // typst font
756            self.system_fonts,
757            self.font_opts(),
758            self.creation_timestamp(),
759            // typst root
760            self.entry_resolver
761                .root(self.entry_resolver.resolve_default().as_ref()),
762        )
763    }
764
765    #[cfg(not(feature = "system"))]
766    fn create_physical_access_model(
767        &self,
768        client: &TypedLspClient<ServerState>,
769    ) -> Arc<dyn LspAccessModel> {
770        self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
771    }
772
773    #[cfg(feature = "system")]
774    fn create_physical_access_model(
775        &self,
776        _client: &TypedLspClient<ServerState>,
777    ) -> Arc<dyn LspAccessModel> {
778        use reflexo_typst::vfs::system::SystemAccessModel;
779        Arc::new(SystemAccessModel {})
780    }
781
782    pub(crate) fn watch_access_model(
783        &self,
784        client: &TypedLspClient<ServerState>,
785    ) -> &Arc<WatchAccessModel> {
786        let client = client.clone();
787        &self
788            .watch_access_model
789            .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
790            .0
791    }
792
793    pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
794        let access_model = || {
795            log::info!(
796                "creating AccessModel with delegation={:?}",
797                self.delegate_fs_requests
798            );
799            if self.delegate_fs_requests {
800                Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
801            } else {
802                Derived(self.create_physical_access_model(client))
803            }
804        };
805        DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
806    }
807}
808
809/// Configuration set at initialization that won't change within a single
810/// session.
811#[derive(Debug, Clone)]
812pub struct ConstConfig {
813    /// Determined position encoding, either UTF-8 or UTF-16.
814    /// Defaults to UTF-16 if not specified.
815    pub position_encoding: PositionEncoding,
816    /// Allow dynamic registration of configuration changes.
817    pub cfg_change_registration: bool,
818    /// Allow notifying workspace/didRenameFiles
819    pub notify_will_rename_files: bool,
820    /// Allow dynamic registration of semantic tokens.
821    pub tokens_dynamic_registration: bool,
822    /// Allow overlapping tokens.
823    pub tokens_overlapping_token_support: bool,
824    /// Allow multiline tokens.
825    pub tokens_multiline_token_support: bool,
826    /// Allow line folding on documents.
827    pub doc_line_folding_only: bool,
828    /// Allow dynamic registration of document formatting.
829    pub doc_fmt_dynamic_registration: bool,
830    /// The locale of the editor.
831    pub locale: Option<String>,
832}
833
834impl Default for ConstConfig {
835    fn default() -> Self {
836        Self::from(&InitializeParams::default())
837    }
838}
839
840impl From<&InitializeParams> for ConstConfig {
841    fn from(params: &InitializeParams) -> Self {
842        // const DEFAULT_ENCODING: &[PositionEncodingKind] =
843        // &[PositionEncodingKind::UTF16];
844
845        // todo: respect position encoding.
846        let position_encoding = {
847            // let general = params.capabilities.general.as_ref();
848            // let encodings = try_(||
849            // Some(general?.position_encodings.as_ref()?.as_slice()));
850            // let encodings = encodings.unwrap_or(DEFAULT_ENCODING);
851
852            // if encodings.contains(&PositionEncodingKind::UTF8) {
853            //     PositionEncoding::Utf8
854            // } else {
855            //     PositionEncoding::Utf16
856            // }
857            PositionEncoding::Utf16
858        };
859
860        let workspace = params.capabilities.workspace.as_ref();
861        let file_operations = try_(|| workspace?.file_operations.as_ref());
862        let doc = params.capabilities.text_document.as_ref();
863        let sema = try_(|| doc?.semantic_tokens.as_ref());
864        let fold = try_(|| doc?.folding_range.as_ref());
865        let format = try_(|| doc?.formatting.as_ref());
866
867        let locale = params
868            .initialization_options
869            .as_ref()
870            .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
871            .or(params.locale.as_deref());
872
873        Self {
874            position_encoding,
875            cfg_change_registration: try_or(|| workspace?.configuration, false),
876            notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
877            tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
878            tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
879            tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
880            doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
881            doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
882            locale: locale.map(ToOwned::to_owned),
883        }
884    }
885}
886
887impl From<&dapts::InitializeRequestArguments> for ConstConfig {
888    fn from(params: &dapts::InitializeRequestArguments) -> Self {
889        let locale = params.locale.as_deref();
890
891        Self {
892            locale: locale.map(ToOwned::to_owned),
893            ..Default::default()
894        }
895    }
896}
897
898/// Determines in what format paths are specified. The default is `path`, which
899/// is the native format.
900pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
901
902/// Configuration set at initialization that won't change within a single DAP
903/// session.
904#[derive(Debug, Clone)]
905pub struct ConstDapConfig {
906    /// The format of paths.
907    pub path_format: DapPathFormat,
908    /// Whether lines start at 1.
909    pub lines_start_at1: bool,
910    /// Whether columns start at 1.
911    pub columns_start_at1: bool,
912}
913
914impl Default for ConstDapConfig {
915    fn default() -> Self {
916        Self::from(&dapts::InitializeRequestArguments::default())
917    }
918}
919
920impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
921    fn from(params: &dapts::InitializeRequestArguments) -> Self {
922        Self {
923            path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
924            lines_start_at1: params.lines_start_at1.unwrap_or(true),
925            columns_start_at1: params.columns_start_at1.unwrap_or(true),
926        }
927    }
928}
929
930/// The mode of the formatter.
931#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub enum FormatterMode {
934    /// Disable the formatter.
935    #[default]
936    Disable,
937    /// Use `typstyle` formatter.
938    Typstyle,
939    /// Use `typstfmt` formatter.
940    Typstfmt,
941}
942
943/// The mode of semantic tokens.
944#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
945#[serde(rename_all = "camelCase")]
946pub enum SemanticTokensMode {
947    /// Disable the semantic tokens.
948    Disable,
949    /// Enable the semantic tokens.
950    #[default]
951    Enable,
952}
953
954/// The preview features.
955#[derive(Debug, Default, Clone, Deserialize)]
956#[serde(rename_all = "camelCase")]
957pub struct PreviewFeat {
958    /// The browsing preview options.
959    #[serde(default, deserialize_with = "deserialize_null_default")]
960    pub browsing: BrowsingPreviewOpts,
961    /// The background preview options.
962    #[serde(default, deserialize_with = "deserialize_null_default")]
963    pub background: BackgroundPreviewOpts,
964    /// When to refresh the preview.
965    #[serde(default)]
966    pub refresh: Option<TaskWhen>,
967    /// Whether to enable partial rendering.
968    #[serde(default, deserialize_with = "deserialize_null_default")]
969    pub partial_rendering: bool,
970    /// Invert colors for the preview.
971    #[cfg(feature = "preview")]
972    #[serde(default, deserialize_with = "deserialize_null_default")]
973    pub invert_colors: PreviewInvertColors,
974}
975
976/// The lint features.
977#[derive(Debug, Default, Clone, Deserialize)]
978pub struct LintFeat {
979    /// Whether to enable linting.
980    pub enabled: Option<bool>,
981    /// When to trigger the lint checks.
982    pub when: Option<TaskWhen>,
983}
984
985impl LintFeat {
986    /// When to trigger the lint checks.
987    pub fn when(&self) -> &TaskWhen {
988        if matches!(self.enabled, Some(false) | None) {
989            return &TaskWhen::Never;
990        }
991
992        self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
993    }
994}
995/// The lint features.
996#[derive(Debug, Default, Clone, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct OnEnterFeat {
999    /// Whether to handle list.
1000    #[serde(default, deserialize_with = "deserialize_null_default")]
1001    pub handle_list: bool,
1002}
1003
1004/// Options for browsing preview.
1005#[derive(Debug, Default, Clone, Deserialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct BrowsingPreviewOpts {
1008    /// The arguments for the `tinymist.startDefaultPreview` command.
1009    pub args: Option<Vec<String>>,
1010}
1011
1012/// Options for background preview.
1013#[derive(Debug, Default, Clone, Deserialize)]
1014#[serde(rename_all = "camelCase")]
1015pub struct BackgroundPreviewOpts {
1016    /// Whether to run the preview in the background.
1017    #[serde(default, deserialize_with = "deserialize_null_default")]
1018    pub enabled: bool,
1019    /// The arguments for the background preview.
1020    pub args: Option<Vec<String>>,
1021}
1022
1023/// The extra typst arguments passed to the language server. You can pass any
1024/// arguments as you like, and we will try to follow behaviors of the **same
1025/// version** of typst-cli.
1026#[derive(Debug, Clone, PartialEq, Default)]
1027pub struct TypstExtraArgs {
1028    /// The root directory for the compilation routine.
1029    pub root_dir: Option<ImmutPath>,
1030    /// The path to the entry.
1031    pub entry: Option<ImmutPath>,
1032    /// The additional input arguments to compile the entry file.
1033    pub inputs: ImmutDict,
1034    /// The additional font paths.
1035    pub font: CompileFontArgs,
1036    /// The package related arguments.
1037    pub package: CompilePackageArgs,
1038    /// One (or multiple comma-separated) PDF standards that Typst will enforce
1039    /// conformance with.
1040    pub features: Vec<Feature>,
1041    /// One (or multiple comma-separated) PDF standards that Typst will enforce
1042    /// conformance with.
1043    pub pdf_standard: Vec<PdfStandard>,
1044    /// The PPI (pixels per inch) to use for PNG export.
1045    pub ppi: f32,
1046    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
1047    /// document is written to provide a baseline of accessibility. In some
1048    /// circumstances (for example when trying to reduce the size of a document)
1049    /// it can be desirable to disable tagged PDF.
1050    pub no_pdf_tags: bool,
1051    /// The creation timestamp for various outputs (in seconds).
1052    pub creation_timestamp: Option<i64>,
1053    /// The path to the certification file.
1054    pub cert: Option<ImmutPath>,
1055}
1056
1057pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
1058    SemanticTokensOptions {
1059        legend: SemanticTokensLegend {
1060            token_types: TokenType::iter()
1061                .filter(|e| *e != TokenType::None)
1062                .map(Into::into)
1063                .collect(),
1064            token_modifiers: Modifier::iter().map(Into::into).collect(),
1065        },
1066        full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
1067        ..SemanticTokensOptions::default()
1068    }
1069}
1070
1071fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1072where
1073    T: Default + Deserialize<'de>,
1074    D: serde::Deserializer<'de>,
1075{
1076    let opt = Option::deserialize(deserializer)?;
1077    Ok(opt.unwrap_or_default())
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082    use super::*;
1083    use serde_json::json;
1084    #[cfg(feature = "preview")]
1085    use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1086
1087    fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1088        temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1089    }
1090
1091    fn good_config(config: &mut Config, update: &JsonValue) {
1092        update_config(config, update).expect("not good");
1093        assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1094    }
1095
1096    #[test]
1097    fn test_default_encoding() {
1098        let cc = ConstConfig::default();
1099        assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1100    }
1101
1102    #[test]
1103    fn test_config_update() {
1104        let mut config = Config::default();
1105
1106        let root_path = Path::new(if cfg!(windows) {
1107            "C:\\dummy-root"
1108        } else {
1109            "/dummy-root"
1110        });
1111
1112        let update = json!({
1113            "outputPath": "out",
1114            "exportPdf": "onSave",
1115            "rootPath": root_path,
1116            "semanticTokens": "enable",
1117            "formatterMode": "typstyle",
1118            "typstExtraArgs": ["--root", root_path]
1119        });
1120
1121        good_config(&mut config, &update);
1122
1123        // Nix specifies this environment variable when testing.
1124        let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1125        if has_source_date_epoch {
1126            let args = config.typst_extra_args.as_mut().unwrap();
1127            assert!(args.creation_timestamp.is_some());
1128            args.creation_timestamp = None;
1129        }
1130
1131        assert_eq!(config.output_path, PathPattern::new("out"));
1132        assert_eq!(config.export_pdf, TaskWhen::OnSave);
1133        assert_eq!(
1134            config.entry_resolver.root_path,
1135            Some(ImmutPath::from(root_path))
1136        );
1137        assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1138        assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1139        assert_eq!(
1140            config.typst_extra_args,
1141            Some(TypstExtraArgs {
1142                root_dir: Some(ImmutPath::from(root_path)),
1143                ppi: 144.0,
1144                ..TypstExtraArgs::default()
1145            })
1146        );
1147    }
1148
1149    #[test]
1150    fn test_namespaced_config() {
1151        let mut config = Config::default();
1152
1153        // Emacs uses a shared configuration object for all language servers.
1154        let update = json!({
1155            "exportPdf": "onSave",
1156            "tinymist": {
1157                "exportPdf": "onType",
1158            }
1159        });
1160
1161        good_config(&mut config, &update);
1162
1163        assert_eq!(config.export_pdf, TaskWhen::OnType);
1164    }
1165
1166    #[test]
1167    fn test_compile_status() {
1168        let mut config = Config::default();
1169
1170        let update = json!({
1171            "compileStatus": "enable",
1172        });
1173        good_config(&mut config, &update);
1174        assert!(config.notify_status);
1175
1176        let update = json!({
1177            "compileStatus": "disable",
1178        });
1179        good_config(&mut config, &update);
1180        assert!(!config.notify_status);
1181    }
1182
1183    #[test]
1184    fn test_config_creation_timestamp() {
1185        type Timestamp = Option<i64>;
1186
1187        fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1188            let mut config = Config::default();
1189
1190            f(&mut config);
1191
1192            let args = config.typst_extra_args;
1193            args.and_then(|args| args.creation_timestamp)
1194        }
1195
1196        // assert!(timestamp(|_| {}).is_none());
1197        // assert!(timestamp(|config| {
1198        //     let update = json!({});
1199        //     good_config(&mut config, &update);
1200        // })
1201        // .is_none());
1202
1203        let args_timestamp = timestamp(|config| {
1204            let update = json!({
1205                "typstExtraArgs": ["--creation-timestamp", "1234"]
1206            });
1207            good_config(config, &update);
1208        });
1209        assert!(args_timestamp.is_some());
1210
1211        // todo: concurrent get/set env vars is unsafe
1212        //     std::env::set_var("SOURCE_DATE_EPOCH", "1234");
1213        //     let env_timestamp = timestamp(|config| {
1214        //         update_config(&mut config, &json!({})).unwrap();
1215        //     });
1216
1217        //     assert_eq!(args_timestamp, env_timestamp);
1218    }
1219
1220    #[test]
1221    fn test_empty_extra_args() {
1222        let mut config = Config::default();
1223        let update = json!({
1224            "typstExtraArgs": []
1225        });
1226
1227        good_config(&mut config, &update);
1228    }
1229
1230    #[test]
1231    fn test_null_args() {
1232        fn test_good_config(path: &str) -> Config {
1233            let mut obj = json!(null);
1234            let path = path.split('.').collect::<Vec<_>>();
1235            for p in path.iter().rev() {
1236                obj = json!({ *p: obj });
1237            }
1238
1239            let mut c = Config::default();
1240            good_config(&mut c, &obj);
1241            c
1242        }
1243
1244        test_good_config("root");
1245        test_good_config("rootPath");
1246        test_good_config("colorTheme");
1247        test_good_config("lint");
1248        test_good_config("customizedShowDocument");
1249        test_good_config("projectResolution");
1250        test_good_config("exportPdf");
1251        test_good_config("exportTarget");
1252        test_good_config("fontPaths");
1253        test_good_config("formatterMode");
1254        test_good_config("formatterPrintWidth");
1255        test_good_config("formatterIndentSize");
1256        test_good_config("formatterProseWrap");
1257        test_good_config("outputPath");
1258        test_good_config("semanticTokens");
1259        test_good_config("delegateFsRequests");
1260        test_good_config("supportHtmlInMarkdown");
1261        test_good_config("supportExtendedCodeAction");
1262        test_good_config("development");
1263        test_good_config("systemFonts");
1264
1265        test_good_config("completion");
1266        test_good_config("completion.triggerSuggest");
1267        test_good_config("completion.triggerParameterHints");
1268        test_good_config("completion.triggerSuggestAndParameterHints");
1269        test_good_config("completion.triggerOnSnippetPlaceholders");
1270        test_good_config("completion.symbol");
1271        test_good_config("completion.postfix");
1272        test_good_config("completion.postfixUfcs");
1273        test_good_config("completion.postfixUfcsLeft");
1274        test_good_config("completion.postfixUfcsRight");
1275        test_good_config("completion.postfixSnippets");
1276
1277        test_good_config("lint");
1278        test_good_config("lint.enabled");
1279        test_good_config("lint.when");
1280
1281        test_good_config("preview");
1282        test_good_config("preview.browsing");
1283        test_good_config("preview.browsing.args");
1284        test_good_config("preview.background");
1285        test_good_config("preview.background.enabled");
1286        test_good_config("preview.background.args");
1287        test_good_config("preview.refresh");
1288        test_good_config("preview.partialRendering");
1289        #[cfg(feature = "preview")]
1290        let c = test_good_config("preview.invertColors");
1291        #[cfg(feature = "preview")]
1292        assert_eq!(
1293            c.preview.invert_colors,
1294            PreviewInvertColors::Enum(PreviewInvertColor::Never)
1295        );
1296    }
1297
1298    #[test]
1299    fn test_font_opts() {
1300        fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1301            let mut config = Config::default();
1302            if let Some(update) = update {
1303                good_config(&mut config, update);
1304            }
1305
1306            config.font_opts()
1307        }
1308
1309        let font_opts = opts(None);
1310        assert!(!font_opts.ignore_system_fonts);
1311
1312        let font_opts = opts(Some(&json!({})));
1313        assert!(!font_opts.ignore_system_fonts);
1314
1315        let font_opts = opts(Some(&json!({
1316            "typstExtraArgs": []
1317        })));
1318        assert!(!font_opts.ignore_system_fonts);
1319
1320        let font_opts = opts(Some(&json!({
1321            "systemFonts": false,
1322        })));
1323        assert!(font_opts.ignore_system_fonts);
1324
1325        let font_opts = opts(Some(&json!({
1326            "typstExtraArgs": ["--ignore-system-fonts"]
1327        })));
1328        assert!(font_opts.ignore_system_fonts);
1329
1330        let font_opts = opts(Some(&json!({
1331            "systemFonts": true,
1332            "typstExtraArgs": ["--ignore-system-fonts"]
1333        })));
1334        assert!(!font_opts.ignore_system_fonts);
1335    }
1336
1337    #[test]
1338    fn test_preview_opts() {
1339        fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1340            let mut config = Config::default();
1341            if let Some(update) = update {
1342                good_config(&mut config, update);
1343            }
1344
1345            config.preview
1346        }
1347
1348        let preview = opts(Some(&json!({
1349            "preview": {
1350            }
1351        })));
1352        assert_eq!(preview.refresh, None);
1353
1354        let preview = opts(Some(&json!({
1355            "preview": {
1356                "refresh":"onType"
1357            }
1358        })));
1359        assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1360
1361        let preview = opts(Some(&json!({
1362            "preview": {
1363                "refresh":"onSave"
1364            }
1365        })));
1366        assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1367    }
1368
1369    #[test]
1370    fn test_reject_abnormal_root() {
1371        let mut config = Config::default();
1372        let update = json!({
1373            "rootPath": ".",
1374        });
1375
1376        let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1377        assert!(err.contains("absolute path"), "unexpected error: {err}");
1378    }
1379
1380    #[test]
1381    fn test_reject_abnormal_root2() {
1382        let mut config = Config::default();
1383        let update = json!({
1384            "typstExtraArgs": ["--root", "."]
1385        });
1386
1387        let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1388        assert!(err.contains("absolute path"), "unexpected error: {err}");
1389    }
1390
1391    #[test]
1392    fn test_entry_by_extra_args() {
1393        let simple_config = {
1394            let mut config = Config::default();
1395            let update = json!({
1396                "typstExtraArgs": ["main.typ"]
1397            });
1398
1399            // It should be able to resolve the entry file from the extra arguments.
1400            update_config(&mut config, &update).expect("updated");
1401            // Passing it twice doesn't affect the result.
1402            update_config(&mut config, &update).expect("updated");
1403            config
1404        };
1405        {
1406            let mut config = Config::default();
1407            let update = json!({
1408                "typstExtraArgs": ["main.typ", "main.typ"]
1409            });
1410            update_config(&mut config, &update).unwrap();
1411            let warns = format!("{:?}", config.warnings);
1412            assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1413            assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1414        }
1415        {
1416            let mut config = Config::default();
1417            let update = json!({
1418                "typstExtraArgs": ["main2.typ"],
1419                "tinymist": {
1420                    "typstExtraArgs": ["main.typ"]
1421                }
1422            });
1423
1424            // It should be able to resolve the entry file from the extra arguments.
1425            update_config(&mut config, &update).expect("updated");
1426            // Passing it twice doesn't affect the result.
1427            update_config(&mut config, &update).expect("updated");
1428
1429            assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1430        }
1431    }
1432
1433    #[test]
1434    fn test_default_formatting_config() {
1435        let config = Config::default().formatter();
1436        assert!(matches!(config.config, FormatterConfig::Disable));
1437        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1438    }
1439
1440    #[test]
1441    fn test_typstyle_formatting_config() {
1442        let config = Config {
1443            formatter_mode: FormatterMode::Typstyle,
1444            ..Config::default()
1445        };
1446        let config = config.formatter();
1447        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1448
1449        let typstyle_config = match config.config {
1450            FormatterConfig::Typstyle(e) => e,
1451            _ => panic!("unexpected configuration of formatter"),
1452        };
1453
1454        assert_eq!(typstyle_config.max_width, 120);
1455    }
1456
1457    #[test]
1458    fn test_typstyle_formatting_config_set_width() {
1459        let config = Config {
1460            formatter_mode: FormatterMode::Typstyle,
1461            formatter_print_width: Some(240),
1462            ..Config::default()
1463        };
1464        let config = config.formatter();
1465        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1466
1467        let typstyle_config = match config.config {
1468            FormatterConfig::Typstyle(e) => e,
1469            _ => panic!("unexpected configuration of formatter"),
1470        };
1471
1472        assert_eq!(typstyle_config.max_width, 240);
1473    }
1474
1475    #[test]
1476    fn test_typstyle_formatting_config_set_tab_spaces() {
1477        let config = Config {
1478            formatter_mode: FormatterMode::Typstyle,
1479            formatter_indent_size: Some(8),
1480            ..Config::default()
1481        };
1482        let config = config.formatter();
1483        assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1484
1485        let typstyle_config = match config.config {
1486            FormatterConfig::Typstyle(e) => e,
1487            _ => panic!("unexpected configuration of formatter"),
1488        };
1489
1490        assert_eq!(typstyle_config.tab_spaces, 8);
1491    }
1492
1493    #[test]
1494    #[cfg(feature = "preview")]
1495    fn test_default_preview_config() {
1496        let config = Config::default().preview();
1497        assert!(!config.enable_partial_rendering);
1498        assert_eq!(config.refresh_style, TaskWhen::OnType);
1499        assert_eq!(config.invert_colors, "\"never\"");
1500    }
1501
1502    #[test]
1503    #[cfg(feature = "preview")]
1504    fn test_preview_config() {
1505        let config = Config {
1506            preview: PreviewFeat {
1507                partial_rendering: true,
1508                refresh: Some(TaskWhen::OnSave),
1509                invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1510                ..PreviewFeat::default()
1511            },
1512            ..Config::default()
1513        }
1514        .preview();
1515
1516        assert!(config.enable_partial_rendering);
1517        assert_eq!(config.refresh_style, TaskWhen::OnSave);
1518        assert_eq!(config.invert_colors, "\"auto\"");
1519    }
1520
1521    #[test]
1522    fn test_default_lsp_config_initialize() {
1523        let (_conf, err) =
1524            Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1525        assert!(err.is_none());
1526    }
1527
1528    #[test]
1529    fn test_default_dap_config_initialize() {
1530        let (_conf, err) = Config::extract_dap_params(
1531            dapts::InitializeRequestArguments::default(),
1532            CompileFontArgs::default(),
1533        );
1534        assert!(err.is_none());
1535    }
1536
1537    #[test]
1538    fn test_config_package_path_from_env() {
1539        let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
1540
1541        temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
1542            let (conf, err) =
1543                Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1544            assert!(err.is_none());
1545            let applied_cache_path = conf
1546                .typst_extra_args
1547                .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
1548            assert!(applied_cache_path);
1549        });
1550    }
1551
1552    #[test]
1553    #[cfg(feature = "preview")]
1554    fn test_invert_colors_validation() {
1555        fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
1556            Ok(serde_json::from_str(s)?)
1557        }
1558
1559        assert_eq!(
1560            test(r#""never""#).unwrap(),
1561            PreviewInvertColors::Enum(PreviewInvertColor::Never)
1562        );
1563        assert_eq!(
1564            test(r#""auto""#).unwrap(),
1565            PreviewInvertColors::Enum(PreviewInvertColor::Auto)
1566        );
1567        assert_eq!(
1568            test(r#""always""#).unwrap(),
1569            PreviewInvertColors::Enum(PreviewInvertColor::Always)
1570        );
1571        assert!(test(r#""e""#).is_err());
1572
1573        assert_eq!(
1574            test(r#"{"rest": "never"}"#).unwrap(),
1575            PreviewInvertColors::Object(PreviewInvertColorObject {
1576                image: PreviewInvertColor::Never,
1577                rest: PreviewInvertColor::Never,
1578            })
1579        );
1580        assert_eq!(
1581            test(r#"{"image": "always"}"#).unwrap(),
1582            PreviewInvertColors::Object(PreviewInvertColorObject {
1583                image: PreviewInvertColor::Always,
1584                rest: PreviewInvertColor::Never,
1585            })
1586        );
1587        assert_eq!(
1588            test(r#"{}"#).unwrap(),
1589            PreviewInvertColors::Object(PreviewInvertColorObject {
1590                image: PreviewInvertColor::Never,
1591                rest: PreviewInvertColor::Never,
1592            })
1593        );
1594        assert_eq!(
1595            test(r#"{"unknown": "ovo"}"#).unwrap(),
1596            PreviewInvertColors::Object(PreviewInvertColorObject {
1597                image: PreviewInvertColor::Never,
1598                rest: PreviewInvertColor::Never,
1599            })
1600        );
1601        assert!(test(r#"{"image": "e"}"#).is_err());
1602    }
1603}