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
42const 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#[derive(Debug, Default, Clone)]
84pub struct Config {
85 pub const_config: ConstConfig,
87 pub const_dap_config: ConstDapConfig,
89
90 pub delegate_fs_requests: bool,
92 pub customized_show_document: bool,
94 pub has_default_entry_path: bool,
96 pub notify_status: bool,
98 pub support_html_in_markdown: bool,
100 pub support_client_codelens: bool,
105 pub extended_code_action: bool,
108 pub development: bool,
110 pub syntax_only: bool,
112
113 pub color_theme: Option<String>,
115 pub entry_resolver: EntryResolver,
117 pub lsp_inputs: ImmutDict,
119 pub periscope_args: Option<PeriscopeArgs>,
121 pub typst_extra_args: Option<TypstExtraArgs>,
123 pub semantic_tokens: SemanticTokensMode,
125
126 pub completion: CompletionFeat,
128 pub preview: PreviewFeat,
130 pub lint: LintFeat,
132 pub on_enter: OnEnterFeat,
134
135 pub font_opts: CompileFontArgs,
137 pub font_paths: Vec<PathBuf>,
139 pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
141 pub system_fonts: Option<bool>,
143
144 pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
146 pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
148
149 pub export_target: ExportTarget,
151 pub export_pdf: TaskWhen,
153 pub output_path: PathPattern,
155
156 pub formatter_mode: FormatterMode,
158 pub formatter_print_width: Option<u32>,
161 pub formatter_indent_size: Option<u32>,
163 pub formatter_prose_wrap: Option<bool>,
165 pub warnings: Vec<CowStr>,
167}
168
169#[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 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 pub fn extract_lsp_params(
212 params: InitializeParams,
213 font_args: CompileFontArgs,
214 ) -> (Self, Option<ResponseError>) {
215 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)] None => params
223 .root_uri
224 .as_ref()
225 .map(|uri| ImmutPath::from(url_to_path(uri)))
226 .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into()))
227 .into_iter()
228 .collect(),
229 };
230 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
231
232 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 pub fn extract_dap_params(
251 params: dapts::InitializeRequestArguments,
252 font_args: CompileFontArgs,
253 ) -> (Self, Option<ResponseError>) {
254 let cwd = std::env::current_dir()
256 .expect("failed to get current directory")
257 .into();
258
259 let roots = vec![cwd];
261 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
262 config.const_dap_config = ConstDapConfig::from(¶ms);
263
264 if let Some(locale) = config.const_config.locale.as_ref() {
266 tinymist_l10n::set_locale(locale);
267 }
268
269 (config, None)
270 }
271
272 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 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 pub fn update(&mut self, update: &JsonValue) -> Result<()> {
304 if let JsonValue::Object(update) = update {
305 self.update_by_map(update)?;
306
307 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 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 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 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 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 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 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 pub fn validate(&self) -> Result<()> {
548 self.entry_resolver.validate()?;
549
550 Ok(())
551 }
552
553 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 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 #[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 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 #[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 task: ProjectTask::ExportPdf(ExportPdfTask {
628 export,
629 pages: None, 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 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 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 pub fn fonts(&self) -> Arc<FontResolverImpl> {
681 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 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 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 pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
728 Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
729 }
730
731 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 pub fn ppi(&self) -> Option<f32> {
740 Some(self.typst_extra_args.as_ref()?.ppi)
741 }
742
743 pub fn creation_timestamp(&self) -> Option<i64> {
745 self.typst_extra_args.as_ref()?.creation_timestamp
746 }
747
748 pub fn certification_path(&self) -> Option<ImmutPath> {
750 self.typst_extra_args.as_ref()?.cert.clone()
751 }
752
753 #[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 self.syntax_only,
772 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 self.certification_path(),
788 self.package_opts(),
789 self.system_fonts,
791 self.font_opts(),
792 self.creation_timestamp(),
793 self.entry_resolver
795 .root(self.entry_resolver.resolve_default().as_ref()),
796 )
797 }
798
799 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#[derive(Debug, Clone)]
863pub struct ConstConfig {
864 pub position_encoding: PositionEncoding,
867 pub cfg_change_registration: bool,
869 pub notify_will_rename_files: bool,
871 pub tokens_dynamic_registration: bool,
873 pub tokens_overlapping_token_support: bool,
875 pub tokens_multiline_token_support: bool,
877 pub doc_line_folding_only: bool,
879 pub doc_fmt_dynamic_registration: bool,
881 pub completion_insert_replace_support: bool,
883 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 let position_encoding = {
900 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
956pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
959
960#[derive(Debug, Clone)]
963pub struct ConstDapConfig {
964 pub path_format: DapPathFormat,
966 pub lines_start_at1: bool,
968 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
990#[serde(rename_all = "camelCase")]
991pub enum FormatterMode {
992 Disable,
994 #[default]
996 Typstyle,
997 Typstfmt,
999}
1000
1001#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
1003#[serde(rename_all = "camelCase")]
1004pub enum SemanticTokensMode {
1005 Disable,
1007 #[default]
1009 Enable,
1010}
1011
1012#[derive(Debug, Default, Clone, Deserialize)]
1014#[serde(rename_all = "camelCase")]
1015pub struct PreviewFeat {
1016 #[serde(default, deserialize_with = "deserialize_null_default")]
1018 pub browsing: BrowsingPreviewOpts,
1019 #[serde(default, deserialize_with = "deserialize_null_default")]
1021 pub background: BackgroundPreviewOpts,
1022 #[serde(default)]
1024 pub refresh: Option<TaskWhen>,
1025 #[serde(default, deserialize_with = "deserialize_null_default")]
1027 pub partial_rendering: bool,
1028 #[cfg(feature = "preview")]
1030 #[serde(default, deserialize_with = "deserialize_null_default")]
1031 pub invert_colors: PreviewInvertColors,
1032}
1033
1034#[derive(Debug, Default, Clone, Deserialize)]
1036pub struct LintFeat {
1037 pub enabled: Option<bool>,
1039 pub when: Option<TaskWhen>,
1041}
1042
1043impl LintFeat {
1044 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#[derive(Debug, Default, Clone, Deserialize)]
1055#[serde(rename_all = "camelCase")]
1056pub struct OnEnterFeat {
1057 #[serde(default, deserialize_with = "deserialize_null_default")]
1059 pub handle_list: bool,
1060}
1061
1062#[derive(Debug, Default, Clone, Deserialize)]
1064#[serde(rename_all = "camelCase")]
1065pub struct BrowsingPreviewOpts {
1066 pub args: Option<Vec<String>>,
1068}
1069
1070#[derive(Debug, Default, Clone, Deserialize)]
1072#[serde(rename_all = "camelCase")]
1073pub struct BackgroundPreviewOpts {
1074 #[serde(default, deserialize_with = "deserialize_null_default")]
1076 pub enabled: bool,
1077 pub args: Option<Vec<String>>,
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Default)]
1085pub struct TypstExtraArgs {
1086 pub root_dir: Option<ImmutPath>,
1088 pub entry: Option<ImmutPath>,
1090 pub inputs: ImmutDict,
1092 pub font: CompileFontArgs,
1094 pub package: CompilePackageArgs,
1096 pub features: Vec<Feature>,
1099 pub pdf_standard: Vec<PdfStandard>,
1102 pub ppi: f32,
1104 pub no_pdf_tags: bool,
1109 pub creation_timestamp: Option<i64>,
1111 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 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 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 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 }
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 update_config(&mut config, &update).expect("updated");
1523 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 update_config(&mut config, &update).expect("updated");
1548 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}