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