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