1use core::fmt;
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, LazyLock, OnceLock};
4
5use clap::Parser;
6use itertools::Itertools;
7use lsp_types::*;
8use reflexo::error::IgnoreLogging;
9use reflexo::CowStr;
10use reflexo_typst::{ImmutPath, TypstDict};
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value as JsonValue};
13use strum::IntoEnumIterator;
14use task::{FormatUserConfig, FormatterConfig};
15use tinymist_l10n::DebugL10n;
16use tinymist_project::{DynAccessModel, LspAccessModel};
17use tinymist_query::analysis::{Modifier, TokenType};
18use tinymist_query::{url_to_path, CompletionFeat, PositionEncoding};
19use tinymist_render::PeriscopeArgs;
20use tinymist_std::error::prelude::*;
21use tinymist_task::ExportTarget;
22use typst::foundations::IntoValue;
23use typst::Features;
24use typst_shim::utils::LazyHash;
25
26use super::*;
27use crate::input::WatchAccessModel;
28use crate::project::{
29 EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen,
30};
31use crate::world::font::FontResolverImpl;
32
33#[cfg(feature = "export")]
34use task::ExportUserConfig;
35#[cfg(feature = "preview")]
36use tinymist_preview::{PreviewConfig, PreviewInvertColors};
37
38#[cfg(feature = "export")]
39use crate::project::{ExportPdfTask, ProjectTask};
40
41const CONFIG_ITEMS: &[&str] = &[
43 "tinymist",
44 "colorTheme",
45 "compileStatus",
46 "lint",
47 "completion",
48 "development",
49 "exportPdf",
50 "exportTarget",
51 "fontPaths",
52 "formatterMode",
53 "formatterPrintWidth",
54 "formatterIndentSize",
55 "formatterProseWrap",
56 "hoverPeriscope",
57 "onEnter",
58 "outputPath",
59 "preview",
60 "projectResolution",
61 "rootPath",
62 "semanticTokens",
63 "systemFonts",
64 "typstExtraArgs",
65];
66#[derive(Debug, Default, Clone)]
74pub struct Config {
75 pub const_config: ConstConfig,
77 pub const_dap_config: ConstDapConfig,
79
80 pub delegate_fs_requests: bool,
82 pub customized_show_document: bool,
84 pub has_default_entry_path: bool,
86 pub notify_status: bool,
88 pub support_html_in_markdown: bool,
90 pub extended_code_action: bool,
93 pub development: bool,
95
96 pub color_theme: Option<String>,
98 pub entry_resolver: EntryResolver,
100 pub lsp_inputs: ImmutDict,
102 pub periscope_args: Option<PeriscopeArgs>,
104 pub typst_extra_args: Option<TypstExtraArgs>,
106 pub semantic_tokens: SemanticTokensMode,
108
109 pub completion: CompletionFeat,
111 pub preview: PreviewFeat,
113 pub lint: LintFeat,
115 pub on_enter: OnEnterFeat,
117
118 pub font_opts: CompileFontArgs,
120 pub font_paths: Vec<PathBuf>,
122 pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
124 pub system_fonts: Option<bool>,
126
127 pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
129 pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
131
132 pub export_target: ExportTarget,
134 pub export_pdf: TaskWhen,
136 pub output_path: PathPattern,
138
139 pub formatter_mode: FormatterMode,
141 pub formatter_print_width: Option<u32>,
144 pub formatter_indent_size: Option<u32>,
146 pub formatter_prose_wrap: Option<bool>,
148 pub warnings: Vec<CowStr>,
150}
151
152impl Config {
153 pub fn new(
155 const_config: ConstConfig,
156 roots: Vec<ImmutPath>,
157 font_opts: CompileFontArgs,
158 ) -> Self {
159 let mut config = Self {
160 const_config,
161 const_dap_config: ConstDapConfig::default(),
162 entry_resolver: EntryResolver {
163 roots,
164 ..EntryResolver::default()
165 },
166 font_opts,
167 ..Self::default()
168 };
169 config
170 .update_by_map(&Map::default())
171 .log_error("failed to assign Config defaults");
172 config
173 }
174
175 pub fn extract_lsp_params(
181 params: InitializeParams,
182 font_args: CompileFontArgs,
183 ) -> (Self, Option<ResponseError>) {
184 let roots = match params.workspace_folders.as_ref() {
186 Some(roots) => roots
187 .iter()
188 .map(|root| ImmutPath::from(url_to_path(&root.uri)))
189 .collect(),
190 #[allow(deprecated)] None => params
192 .root_uri
193 .as_ref()
194 .map(|uri| ImmutPath::from(url_to_path(uri)))
195 .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into()))
196 .into_iter()
197 .collect(),
198 };
199 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
200
201 if let Some(locale) = config.const_config.locale.as_ref() {
203 tinymist_l10n::set_locale(locale);
204 }
205
206 let err = params
207 .initialization_options
208 .and_then(|init| config.update(&init).map_err(invalid_params).err());
209
210 (config, err)
211 }
212
213 pub fn extract_dap_params(
219 params: dapts::InitializeRequestArguments,
220 font_args: CompileFontArgs,
221 ) -> (Self, Option<ResponseError>) {
222 let cwd = std::env::current_dir()
224 .expect("failed to get current directory")
225 .into();
226
227 let roots = vec![cwd];
229 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
230 config.const_dap_config = ConstDapConfig::from(¶ms);
231
232 if let Some(locale) = config.const_config.locale.as_ref() {
234 tinymist_l10n::set_locale(locale);
235 }
236
237 (config, None)
238 }
239
240 pub fn get_items() -> Vec<ConfigurationItem> {
243 CONFIG_ITEMS
244 .iter()
245 .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
246 .map(|section| ConfigurationItem {
247 section: Some(section),
248 ..ConfigurationItem::default()
249 })
250 .collect()
251 }
252
253 pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
255 let unpaired_values = values
256 .into_iter()
257 .tuples()
258 .map(|(a, b)| if !a.is_null() { a } else { b });
259
260 CONFIG_ITEMS
261 .iter()
262 .map(|&item| item.to_owned())
263 .zip(unpaired_values)
264 .collect()
265 }
266
267 pub fn update(&mut self, update: &JsonValue) -> Result<()> {
272 if let JsonValue::Object(update) = update {
273 self.update_by_map(update)?;
274
275 if let Some(namespaced) = update.get("tinymist").and_then(JsonValue::as_object) {
277 self.update_by_map(namespaced)?;
278 }
279
280 Ok(())
281 } else {
282 tinymist_l10n::bail!(
283 "tinymist.config.invalidObject",
284 "invalid configuration object: {object}",
285 object = update.debug_l10n(),
286 )
287 }
288 }
289
290 pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> Result<()> {
295 log::info!(
296 "ServerState: config update_by_map {}",
297 serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
298 );
299
300 self.warnings.clear();
301
302 macro_rules! try_deserialize {
303 ($ty:ty, $key:expr) => {
304 update.get($key).and_then(|v| {
305 <$ty>::deserialize(v)
306 .inspect_err(|err| {
307 if v.is_null() {
310 return;
311 }
312
313 self.warnings.push(tinymist_l10n::t!(
314 "tinymist.config.deserializeError",
315 "failed to deserialize \"{key}\": {err}",
316 key = $key.debug_l10n(),
317 err = err.debug_l10n(),
318 ));
319 })
320 .ok()
321 })
322 };
323 }
324
325 macro_rules! assign_config {
326 ($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
327 let v = try_deserialize!($ty, $bind);
328 self.$($field_path).+ = v.unwrap_or_default();
329 };
330 ($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
331 let v = try_deserialize!($ty, $bind);
332 self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
333 };
334 }
335
336 assign_config!(color_theme := "colorTheme"?: Option<String>);
337 assign_config!(lint := "lint"?: LintFeat);
338 assign_config!(completion := "completion"?: CompletionFeat);
339 assign_config!(on_enter := "onEnter"?: OnEnterFeat);
340 assign_config!(completion.trigger_suggest := "triggerSuggest"?: bool);
341 assign_config!(completion.trigger_parameter_hints := "triggerParameterHints"?: bool);
342 assign_config!(completion.trigger_suggest_and_parameter_hints := "triggerSuggestAndParameterHints"?: bool);
343 assign_config!(customized_show_document := "customizedShowDocument"?: bool);
344 assign_config!(entry_resolver.project_resolution := "projectResolution"?: ProjectResolutionKind);
345 assign_config!(export_pdf := "exportPdf"?: TaskWhen);
346 assign_config!(export_target := "exportTarget"?: ExportTarget);
347 assign_config!(font_paths := "fontPaths"?: Vec<_>);
348 assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
349 assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
350 assign_config!(formatter_indent_size := "formatterIndentSize"?: Option<u32>);
351 assign_config!(formatter_prose_wrap := "formatterProseWrap"?: Option<bool>);
352 assign_config!(output_path := "outputPath"?: PathPattern);
353 assign_config!(preview := "preview"?: PreviewFeat);
354 assign_config!(lint := "lint"?: LintFeat);
355 assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
356 assign_config!(delegate_fs_requests := "delegateFsRequests"?: bool);
357 assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
358 assign_config!(extended_code_action := "supportExtendedCodeAction"?: bool);
359 assign_config!(development := "development"?: bool);
360 assign_config!(system_fonts := "systemFonts"?: Option<bool>);
361
362 self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
363 Some("enable") => true,
364 Some("disable") | None => false,
365 Some(value) => {
366 self.warnings.push(tinymist_l10n::t!(
367 "tinymist.config.badCompileStatus",
368 "compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
369 value = value.debug_l10n(),
370 ));
371
372 false
373 }
374 };
375
376 self.periscope_args = match update.get("hoverPeriscope") {
378 Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()),
379 Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None,
380 Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
381 Ok(args) => Some(args),
382 Err(err) => {
383 self.warnings.push(tinymist_l10n::t!(
384 "tinymist.config.badHoverPeriscope",
385 "failed to parse hoverPeriscope: {err}",
386 err = err.debug_l10n(),
387 ));
388 None
389 }
390 },
391 };
392 if let Some(args) = self.periscope_args.as_mut() {
393 if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") {
394 "always".clone_into(&mut args.invert_color);
395 }
396 }
397
398 fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> CowStr {
399 log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
400 tinymist_l10n::t!(
401 "tinymist.config.badTypstExtraArgs",
402 "failed to parse typstExtraArgs: {err}, args: {args}",
403 err = err.debug_l10n(),
404 args = args.debug_l10n(),
405 )
406 }
407
408 {
409 let raw_args = || update.get("typstExtraArgs");
410 let typst_args: Vec<String> = match raw_args().cloned().map(serde_json::from_value) {
411 Some(Ok(args)) => args,
412 Some(Err(err)) => {
413 self.warnings.push(invalid_extra_args(&raw_args(), err));
414 None
415 }
416 None => None,
419 }
420 .unwrap_or_default();
421 let empty_typst_args = typst_args.is_empty();
422
423 let args = match CompileOnceArgs::try_parse_from(
424 Some("typst-cli".to_owned()).into_iter().chain(typst_args),
425 ) {
426 Ok(args) => args,
427 Err(err) => {
428 self.warnings.push(invalid_extra_args(&raw_args(), err));
429
430 if empty_typst_args {
431 CompileOnceArgs::default()
432 } else {
433 CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
435 .inspect_err(|err| {
436 log::error!("failed to make default typstExtraArgs: {err}");
437 })
438 .unwrap_or_default()
439 }
440 }
441 };
442
443 self.typst_extra_args = Some(TypstExtraArgs {
445 inputs: args.resolve_inputs().unwrap_or_default(),
446 entry: args.input.map(|e| Path::new(&e).into()),
447 root_dir: args.root.as_ref().map(|r| r.as_path().into()),
448 font: args.font,
449 package: args.package,
450 pdf_standard: args.pdf_standard,
451 features: args.features,
452 creation_timestamp: args.creation_timestamp,
453 cert: args.cert.as_deref().map(From::from),
454 });
455 }
456
457 self.entry_resolver.root_path =
458 try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
459 self.typst_extra_args
460 .as_ref()
461 .and_then(|e| e.root_dir.clone())
462 });
463 self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
464 self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
465 self.lsp_inputs = {
466 let mut dict = TypstDict::default();
467
468 #[derive(Serialize)]
469 #[serde(rename_all = "camelCase")]
470 struct PreviewInputs {
471 pub version: u32,
472 pub theme: String,
473 }
474
475 dict.insert(
476 "x-preview".into(),
477 serde_json::to_string(&PreviewInputs {
478 version: 1,
479 theme: self.color_theme.clone().unwrap_or_default(),
480 })
481 .unwrap()
482 .into_value(),
483 );
484
485 Arc::new(LazyHash::new(dict))
486 };
487
488 self.validate()
489 }
490
491 pub fn validate(&self) -> Result<()> {
493 self.entry_resolver.validate()?;
494
495 Ok(())
496 }
497
498 pub fn formatter(&self) -> FormatUserConfig {
500 let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
501 let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
502 let formatter_line_wrap = self.formatter_prose_wrap.unwrap_or(false);
503
504 FormatUserConfig {
505 config: match self.formatter_mode {
506 FormatterMode::Typstyle => {
507 FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
508 tab_spaces: formatter_indent_size,
509 max_width: formatter_print_width,
510 wrap_text: formatter_line_wrap,
511 ..typstyle_core::Config::default()
512 }))
513 }
514 FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
515 max_line_length: formatter_print_width,
516 indent_space: formatter_indent_size,
517 line_wrap: formatter_line_wrap,
518 ..typstfmt::Config::default()
519 })),
520 FormatterMode::Disable => FormatterConfig::Disable,
521 },
522 position_encoding: self.const_config.position_encoding,
523 }
524 }
525
526 #[cfg(feature = "preview")]
528 pub fn preview(&self) -> PreviewConfig {
529 PreviewConfig {
530 enable_partial_rendering: self.preview.partial_rendering,
531 refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
532 invert_colors: serde_json::to_string(&self.preview.invert_colors)
533 .unwrap_or_else(|_| "never".to_string()),
534 }
535 }
536
537 pub(crate) fn export_task(&self) -> ExportTask {
539 ExportTask {
540 when: self.export_pdf.clone(),
541 output: Some(self.output_path.clone()),
542 transform: vec![],
543 }
544 }
545
546 #[cfg(feature = "export")]
548 pub(crate) fn export(&self) -> ExportUserConfig {
549 let export = self.export_task();
550 ExportUserConfig {
551 export_target: self.export_target,
552 task: ProjectTask::ExportPdf(ExportPdfTask {
562 export,
563 pdf_standards: self.pdf_standards().unwrap_or_default(),
564 creation_timestamp: self.creation_timestamp(),
565 }),
566 count_words: self.notify_status,
567 development: self.development,
568 }
569 }
570
571 pub fn font_opts(&self) -> CompileFontArgs {
573 let mut opts = self.font_opts.clone();
574
575 if let Some(system_fonts) = self.system_fonts.or_else(|| {
576 self.typst_extra_args
577 .as_ref()
578 .map(|x| !x.font.ignore_system_fonts)
579 }) {
580 opts.ignore_system_fonts = !system_fonts;
581 }
582
583 let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
584 let font_paths =
585 font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
586 if let Some(paths) = font_paths {
587 opts.font_paths.clone_from(paths);
588 }
589
590 let root = OnceLock::new();
591 for path in opts.font_paths.iter_mut() {
592 if path.is_relative() {
593 if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
594 let p = std::mem::take(path);
595 *path = root.join(p);
596 }
597 }
598 }
599
600 opts
601 }
602
603 pub fn package_opts(&self) -> CompilePackageArgs {
605 if let Some(extras) = &self.typst_extra_args {
606 return extras.package.clone();
607 }
608 CompilePackageArgs::default()
609 }
610
611 pub fn fonts(&self) -> Arc<FontResolverImpl> {
613 let font = || {
615 let opts = self.font_opts();
616
617 log::info!("creating SharedFontResolver with {opts:?}");
618 Derived(
619 crate::project::LspUniverseBuilder::resolve_fonts(opts)
620 .map(Arc::new)
621 .expect("failed to create font book"),
622 )
623 };
624 self.fonts.get_or_init(font).clone().0
625 }
626
627 pub fn inputs(&self) -> ImmutDict {
629 #[comemo::memoize]
630 fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
631 let mut dict = (**lhs).clone();
632 for (k, v) in rhs.iter() {
633 dict.insert(k.clone(), v.clone());
634 }
635
636 Arc::new(LazyHash::new(dict))
637 }
638
639 combine(self.user_inputs(), self.lsp_inputs.clone())
640 }
641
642 fn user_inputs(&self) -> ImmutDict {
643 static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
644
645 if let Some(extras) = &self.typst_extra_args {
646 return extras.inputs.clone();
647 }
648
649 EMPTY.clone()
650 }
651
652 pub fn typst_features(&self) -> Option<Features> {
654 let features = &self.typst_extra_args.as_ref()?.features;
655 Some(Features::from_iter(features.iter().map(|f| (*f).into())))
656 }
657
658 pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
660 Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
661 }
662
663 pub fn creation_timestamp(&self) -> Option<i64> {
665 self.typst_extra_args.as_ref()?.creation_timestamp
666 }
667
668 pub fn certification_path(&self) -> Option<ImmutPath> {
670 self.typst_extra_args.as_ref()?.cert.clone()
671 }
672
673 #[allow(clippy::type_complexity)]
675 pub fn primary_opts(
676 &self,
677 ) -> (
678 Option<bool>,
679 &Vec<PathBuf>,
680 Option<&CompileFontArgs>,
681 Option<i64>,
682 Option<Arc<Path>>,
683 ) {
684 (
685 self.system_fonts,
686 &self.font_paths,
687 self.typst_extra_args.as_ref().map(|e| &e.font),
688 self.creation_timestamp(),
689 self.entry_resolver
690 .root(self.entry_resolver.resolve_default().as_ref()),
691 )
692 }
693
694 #[cfg(not(feature = "system"))]
695 fn create_physical_access_model(
696 &self,
697 client: &TypedLspClient<ServerState>,
698 ) -> Arc<dyn LspAccessModel> {
699 self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
700 }
701
702 #[cfg(feature = "system")]
703 fn create_physical_access_model(
704 &self,
705 _client: &TypedLspClient<ServerState>,
706 ) -> Arc<dyn LspAccessModel> {
707 use reflexo_typst::vfs::system::SystemAccessModel;
708 Arc::new(SystemAccessModel {})
709 }
710
711 pub(crate) fn watch_access_model(
712 &self,
713 client: &TypedLspClient<ServerState>,
714 ) -> &Arc<WatchAccessModel> {
715 let client = client.clone();
716 &self
717 .watch_access_model
718 .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
719 .0
720 }
721
722 pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
723 let access_model = || {
724 log::info!(
725 "creating AccessModel with delegation={:?}",
726 self.delegate_fs_requests
727 );
728 if self.delegate_fs_requests {
729 Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
730 } else {
731 Derived(self.create_physical_access_model(client))
732 }
733 };
734 DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
735 }
736}
737
738#[derive(Debug, Clone)]
741pub struct ConstConfig {
742 pub position_encoding: PositionEncoding,
745 pub cfg_change_registration: bool,
747 pub notify_will_rename_files: bool,
749 pub tokens_dynamic_registration: bool,
751 pub tokens_overlapping_token_support: bool,
753 pub tokens_multiline_token_support: bool,
755 pub doc_line_folding_only: bool,
757 pub doc_fmt_dynamic_registration: bool,
759 pub locale: Option<String>,
761}
762
763impl Default for ConstConfig {
764 fn default() -> Self {
765 Self::from(&InitializeParams::default())
766 }
767}
768
769impl From<&InitializeParams> for ConstConfig {
770 fn from(params: &InitializeParams) -> Self {
771 const DEFAULT_ENCODING: &[PositionEncodingKind] = &[PositionEncodingKind::UTF16];
772
773 let position_encoding = {
774 let general = params.capabilities.general.as_ref();
775 let encodings = try_(|| Some(general?.position_encodings.as_ref()?.as_slice()));
776 let encodings = encodings.unwrap_or(DEFAULT_ENCODING);
777
778 if encodings.contains(&PositionEncodingKind::UTF8) {
779 PositionEncoding::Utf8
780 } else {
781 PositionEncoding::Utf16
782 }
783 };
784
785 let workspace = params.capabilities.workspace.as_ref();
786 let file_operations = try_(|| workspace?.file_operations.as_ref());
787 let doc = params.capabilities.text_document.as_ref();
788 let sema = try_(|| doc?.semantic_tokens.as_ref());
789 let fold = try_(|| doc?.folding_range.as_ref());
790 let format = try_(|| doc?.formatting.as_ref());
791
792 let locale = params
793 .initialization_options
794 .as_ref()
795 .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
796 .or(params.locale.as_deref());
797
798 Self {
799 position_encoding,
800 cfg_change_registration: try_or(|| workspace?.configuration, false),
801 notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
802 tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
803 tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
804 tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
805 doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
806 doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
807 locale: locale.map(ToOwned::to_owned),
808 }
809 }
810}
811
812impl From<&dapts::InitializeRequestArguments> for ConstConfig {
813 fn from(params: &dapts::InitializeRequestArguments) -> Self {
814 let locale = params.locale.as_deref();
815
816 Self {
817 locale: locale.map(ToOwned::to_owned),
818 ..Default::default()
819 }
820 }
821}
822
823pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
826
827#[derive(Debug, Clone)]
830pub struct ConstDapConfig {
831 pub path_format: DapPathFormat,
833 pub lines_start_at1: bool,
835 pub columns_start_at1: bool,
837}
838
839impl Default for ConstDapConfig {
840 fn default() -> Self {
841 Self::from(&dapts::InitializeRequestArguments::default())
842 }
843}
844
845impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
846 fn from(params: &dapts::InitializeRequestArguments) -> Self {
847 Self {
848 path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
849 lines_start_at1: params.lines_start_at1.unwrap_or(true),
850 columns_start_at1: params.columns_start_at1.unwrap_or(true),
851 }
852 }
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
857#[serde(rename_all = "camelCase")]
858pub enum FormatterMode {
859 #[default]
861 Disable,
862 Typstyle,
864 Typstfmt,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
870#[serde(rename_all = "camelCase")]
871pub enum SemanticTokensMode {
872 Disable,
874 #[default]
876 Enable,
877}
878
879#[derive(Debug, Default, Clone, Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct PreviewFeat {
883 #[serde(default, deserialize_with = "deserialize_null_default")]
885 pub browsing: BrowsingPreviewOpts,
886 #[serde(default, deserialize_with = "deserialize_null_default")]
888 pub background: BackgroundPreviewOpts,
889 #[serde(default)]
891 pub refresh: Option<TaskWhen>,
892 #[serde(default, deserialize_with = "deserialize_null_default")]
894 pub partial_rendering: bool,
895 #[cfg(feature = "preview")]
897 #[serde(default, deserialize_with = "deserialize_null_default")]
898 pub invert_colors: PreviewInvertColors,
899}
900
901#[derive(Debug, Default, Clone, Deserialize)]
903pub struct LintFeat {
904 pub enabled: Option<bool>,
906 pub when: Option<TaskWhen>,
908}
909
910impl LintFeat {
911 pub fn when(&self) -> &TaskWhen {
913 if matches!(self.enabled, Some(false) | None) {
914 return &TaskWhen::Never;
915 }
916
917 self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
918 }
919}
920#[derive(Debug, Default, Clone, Deserialize)]
922#[serde(rename_all = "camelCase")]
923pub struct OnEnterFeat {
924 #[serde(default, deserialize_with = "deserialize_null_default")]
926 pub handle_list: bool,
927}
928
929#[derive(Debug, Default, Clone, Deserialize)]
931#[serde(rename_all = "camelCase")]
932pub struct BrowsingPreviewOpts {
933 pub args: Option<Vec<String>>,
935}
936
937#[derive(Debug, Default, Clone, Deserialize)]
939#[serde(rename_all = "camelCase")]
940pub struct BackgroundPreviewOpts {
941 #[serde(default, deserialize_with = "deserialize_null_default")]
943 pub enabled: bool,
944 pub args: Option<Vec<String>>,
946}
947
948#[derive(Debug, Clone, PartialEq, Default)]
952pub struct TypstExtraArgs {
953 pub root_dir: Option<ImmutPath>,
955 pub entry: Option<ImmutPath>,
957 pub inputs: ImmutDict,
959 pub font: CompileFontArgs,
961 pub package: CompilePackageArgs,
963 pub features: Vec<Feature>,
966 pub pdf_standard: Vec<PdfStandard>,
969 pub creation_timestamp: Option<i64>,
971 pub cert: Option<ImmutPath>,
973}
974
975pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
976 SemanticTokensOptions {
977 legend: SemanticTokensLegend {
978 token_types: TokenType::iter()
979 .filter(|e| *e != TokenType::None)
980 .map(Into::into)
981 .collect(),
982 token_modifiers: Modifier::iter().map(Into::into).collect(),
983 },
984 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
985 ..SemanticTokensOptions::default()
986 }
987}
988
989fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
990where
991 T: Default + Deserialize<'de>,
992 D: serde::Deserializer<'de>,
993{
994 let opt = Option::deserialize(deserializer)?;
995 Ok(opt.unwrap_or_default())
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001 use serde_json::json;
1002 #[cfg(feature = "preview")]
1003 use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1004
1005 fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1006 temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1007 }
1008
1009 fn good_config(config: &mut Config, update: &JsonValue) {
1010 update_config(config, update).expect("not good");
1011 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1012 }
1013
1014 #[test]
1015 fn test_default_encoding() {
1016 let cc = ConstConfig::default();
1017 assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1018 }
1019
1020 #[test]
1021 fn test_config_update() {
1022 let mut config = Config::default();
1023
1024 let root_path = Path::new(if cfg!(windows) {
1025 "C:\\dummy-root"
1026 } else {
1027 "/dummy-root"
1028 });
1029
1030 let update = json!({
1031 "outputPath": "out",
1032 "exportPdf": "onSave",
1033 "rootPath": root_path,
1034 "semanticTokens": "enable",
1035 "formatterMode": "typstyle",
1036 "typstExtraArgs": ["--root", root_path]
1037 });
1038
1039 good_config(&mut config, &update);
1040
1041 let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1043 if has_source_date_epoch {
1044 let args = config.typst_extra_args.as_mut().unwrap();
1045 assert!(args.creation_timestamp.is_some());
1046 args.creation_timestamp = None;
1047 }
1048
1049 assert_eq!(config.output_path, PathPattern::new("out"));
1050 assert_eq!(config.export_pdf, TaskWhen::OnSave);
1051 assert_eq!(
1052 config.entry_resolver.root_path,
1053 Some(ImmutPath::from(root_path))
1054 );
1055 assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1056 assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1057 assert_eq!(
1058 config.typst_extra_args,
1059 Some(TypstExtraArgs {
1060 root_dir: Some(ImmutPath::from(root_path)),
1061 ..TypstExtraArgs::default()
1062 })
1063 );
1064 }
1065
1066 #[test]
1067 fn test_namespaced_config() {
1068 let mut config = Config::default();
1069
1070 let update = json!({
1072 "exportPdf": "onSave",
1073 "tinymist": {
1074 "exportPdf": "onType",
1075 }
1076 });
1077
1078 good_config(&mut config, &update);
1079
1080 assert_eq!(config.export_pdf, TaskWhen::OnType);
1081 }
1082
1083 #[test]
1084 fn test_compile_status() {
1085 let mut config = Config::default();
1086
1087 let update = json!({
1088 "compileStatus": "enable",
1089 });
1090 good_config(&mut config, &update);
1091 assert!(config.notify_status);
1092
1093 let update = json!({
1094 "compileStatus": "disable",
1095 });
1096 good_config(&mut config, &update);
1097 assert!(!config.notify_status);
1098 }
1099
1100 #[test]
1101 fn test_config_creation_timestamp() {
1102 type Timestamp = Option<i64>;
1103
1104 fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1105 let mut config = Config::default();
1106
1107 f(&mut config);
1108
1109 let args = config.typst_extra_args;
1110 args.and_then(|args| args.creation_timestamp)
1111 }
1112
1113 let args_timestamp = timestamp(|config| {
1121 let update = json!({
1122 "typstExtraArgs": ["--creation-timestamp", "1234"]
1123 });
1124 good_config(config, &update);
1125 });
1126 assert!(args_timestamp.is_some());
1127
1128 }
1136
1137 #[test]
1138 fn test_empty_extra_args() {
1139 let mut config = Config::default();
1140 let update = json!({
1141 "typstExtraArgs": []
1142 });
1143
1144 good_config(&mut config, &update);
1145 }
1146
1147 #[test]
1148 fn test_null_args() {
1149 fn test_good_config(path: &str) -> Config {
1150 let mut obj = json!(null);
1151 let path = path.split('.').collect::<Vec<_>>();
1152 for p in path.iter().rev() {
1153 obj = json!({ *p: obj });
1154 }
1155
1156 let mut c = Config::default();
1157 good_config(&mut c, &obj);
1158 c
1159 }
1160
1161 test_good_config("root");
1162 test_good_config("rootPath");
1163 test_good_config("colorTheme");
1164 test_good_config("lint");
1165 test_good_config("customizedShowDocument");
1166 test_good_config("projectResolution");
1167 test_good_config("exportPdf");
1168 test_good_config("exportTarget");
1169 test_good_config("fontPaths");
1170 test_good_config("formatterMode");
1171 test_good_config("formatterPrintWidth");
1172 test_good_config("formatterIndentSize");
1173 test_good_config("formatterProseWrap");
1174 test_good_config("outputPath");
1175 test_good_config("semanticTokens");
1176 test_good_config("delegateFsRequests");
1177 test_good_config("supportHtmlInMarkdown");
1178 test_good_config("supportExtendedCodeAction");
1179 test_good_config("development");
1180 test_good_config("systemFonts");
1181
1182 test_good_config("completion");
1183 test_good_config("completion.triggerSuggest");
1184 test_good_config("completion.triggerParameterHints");
1185 test_good_config("completion.triggerSuggestAndParameterHints");
1186 test_good_config("completion.triggerOnSnippetPlaceholders");
1187 test_good_config("completion.symbol");
1188 test_good_config("completion.postfix");
1189 test_good_config("completion.postfixUfcs");
1190 test_good_config("completion.postfixUfcsLeft");
1191 test_good_config("completion.postfixUfcsRight");
1192 test_good_config("completion.postfixSnippets");
1193
1194 test_good_config("lint");
1195 test_good_config("lint.enabled");
1196 test_good_config("lint.when");
1197
1198 test_good_config("preview");
1199 test_good_config("preview.browsing");
1200 test_good_config("preview.browsing.args");
1201 test_good_config("preview.background");
1202 test_good_config("preview.background.enabled");
1203 test_good_config("preview.background.args");
1204 test_good_config("preview.refresh");
1205 test_good_config("preview.partialRendering");
1206 #[cfg(feature = "preview")]
1207 let c = test_good_config("preview.invertColors");
1208 #[cfg(feature = "preview")]
1209 assert_eq!(
1210 c.preview.invert_colors,
1211 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1212 );
1213 }
1214
1215 #[test]
1216 fn test_font_opts() {
1217 fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1218 let mut config = Config::default();
1219 if let Some(update) = update {
1220 good_config(&mut config, update);
1221 }
1222
1223 config.font_opts()
1224 }
1225
1226 let font_opts = opts(None);
1227 assert!(!font_opts.ignore_system_fonts);
1228
1229 let font_opts = opts(Some(&json!({})));
1230 assert!(!font_opts.ignore_system_fonts);
1231
1232 let font_opts = opts(Some(&json!({
1233 "typstExtraArgs": []
1234 })));
1235 assert!(!font_opts.ignore_system_fonts);
1236
1237 let font_opts = opts(Some(&json!({
1238 "systemFonts": false,
1239 })));
1240 assert!(font_opts.ignore_system_fonts);
1241
1242 let font_opts = opts(Some(&json!({
1243 "typstExtraArgs": ["--ignore-system-fonts"]
1244 })));
1245 assert!(font_opts.ignore_system_fonts);
1246
1247 let font_opts = opts(Some(&json!({
1248 "systemFonts": true,
1249 "typstExtraArgs": ["--ignore-system-fonts"]
1250 })));
1251 assert!(!font_opts.ignore_system_fonts);
1252 }
1253
1254 #[test]
1255 fn test_preview_opts() {
1256 fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1257 let mut config = Config::default();
1258 if let Some(update) = update {
1259 good_config(&mut config, update);
1260 }
1261
1262 config.preview
1263 }
1264
1265 let preview = opts(Some(&json!({
1266 "preview": {
1267 }
1268 })));
1269 assert_eq!(preview.refresh, None);
1270
1271 let preview = opts(Some(&json!({
1272 "preview": {
1273 "refresh":"onType"
1274 }
1275 })));
1276 assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1277
1278 let preview = opts(Some(&json!({
1279 "preview": {
1280 "refresh":"onSave"
1281 }
1282 })));
1283 assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1284 }
1285
1286 #[test]
1287 fn test_reject_abnormal_root() {
1288 let mut config = Config::default();
1289 let update = json!({
1290 "rootPath": ".",
1291 });
1292
1293 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1294 assert!(err.contains("absolute path"), "unexpected error: {err}");
1295 }
1296
1297 #[test]
1298 fn test_reject_abnormal_root2() {
1299 let mut config = Config::default();
1300 let update = json!({
1301 "typstExtraArgs": ["--root", "."]
1302 });
1303
1304 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1305 assert!(err.contains("absolute path"), "unexpected error: {err}");
1306 }
1307
1308 #[test]
1309 fn test_entry_by_extra_args() {
1310 let simple_config = {
1311 let mut config = Config::default();
1312 let update = json!({
1313 "typstExtraArgs": ["main.typ"]
1314 });
1315
1316 update_config(&mut config, &update).expect("updated");
1318 update_config(&mut config, &update).expect("updated");
1320 config
1321 };
1322 {
1323 let mut config = Config::default();
1324 let update = json!({
1325 "typstExtraArgs": ["main.typ", "main.typ"]
1326 });
1327 update_config(&mut config, &update).unwrap();
1328 let warns = format!("{:?}", config.warnings);
1329 assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1330 assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1331 }
1332 {
1333 let mut config = Config::default();
1334 let update = json!({
1335 "typstExtraArgs": ["main2.typ"],
1336 "tinymist": {
1337 "typstExtraArgs": ["main.typ"]
1338 }
1339 });
1340
1341 update_config(&mut config, &update).expect("updated");
1343 update_config(&mut config, &update).expect("updated");
1345
1346 assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1347 }
1348 }
1349
1350 #[test]
1351 fn test_default_formatting_config() {
1352 let config = Config::default().formatter();
1353 assert!(matches!(config.config, FormatterConfig::Disable));
1354 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1355 }
1356
1357 #[test]
1358 fn test_typstyle_formatting_config() {
1359 let config = Config {
1360 formatter_mode: FormatterMode::Typstyle,
1361 ..Config::default()
1362 };
1363 let config = config.formatter();
1364 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1365
1366 let typstyle_config = match config.config {
1367 FormatterConfig::Typstyle(e) => e,
1368 _ => panic!("unexpected configuration of formatter"),
1369 };
1370
1371 assert_eq!(typstyle_config.max_width, 120);
1372 }
1373
1374 #[test]
1375 fn test_typstyle_formatting_config_set_width() {
1376 let config = Config {
1377 formatter_mode: FormatterMode::Typstyle,
1378 formatter_print_width: Some(240),
1379 ..Config::default()
1380 };
1381 let config = config.formatter();
1382 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1383
1384 let typstyle_config = match config.config {
1385 FormatterConfig::Typstyle(e) => e,
1386 _ => panic!("unexpected configuration of formatter"),
1387 };
1388
1389 assert_eq!(typstyle_config.max_width, 240);
1390 }
1391
1392 #[test]
1393 fn test_typstyle_formatting_config_set_tab_spaces() {
1394 let config = Config {
1395 formatter_mode: FormatterMode::Typstyle,
1396 formatter_indent_size: Some(8),
1397 ..Config::default()
1398 };
1399 let config = config.formatter();
1400 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1401
1402 let typstyle_config = match config.config {
1403 FormatterConfig::Typstyle(e) => e,
1404 _ => panic!("unexpected configuration of formatter"),
1405 };
1406
1407 assert_eq!(typstyle_config.tab_spaces, 8);
1408 }
1409
1410 #[test]
1411 #[cfg(feature = "preview")]
1412 fn test_default_preview_config() {
1413 let config = Config::default().preview();
1414 assert!(!config.enable_partial_rendering);
1415 assert_eq!(config.refresh_style, TaskWhen::OnType);
1416 assert_eq!(config.invert_colors, "\"never\"");
1417 }
1418
1419 #[test]
1420 #[cfg(feature = "preview")]
1421 fn test_preview_config() {
1422 let config = Config {
1423 preview: PreviewFeat {
1424 partial_rendering: true,
1425 refresh: Some(TaskWhen::OnSave),
1426 invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1427 ..PreviewFeat::default()
1428 },
1429 ..Config::default()
1430 }
1431 .preview();
1432
1433 assert!(config.enable_partial_rendering);
1434 assert_eq!(config.refresh_style, TaskWhen::OnSave);
1435 assert_eq!(config.invert_colors, "\"auto\"");
1436 }
1437
1438 #[test]
1439 fn test_default_lsp_config_initialize() {
1440 let (_conf, err) =
1441 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1442 assert!(err.is_none());
1443 }
1444
1445 #[test]
1446 fn test_default_dap_config_initialize() {
1447 let (_conf, err) = Config::extract_dap_params(
1448 dapts::InitializeRequestArguments::default(),
1449 CompileFontArgs::default(),
1450 );
1451 assert!(err.is_none());
1452 }
1453
1454 #[test]
1455 fn test_config_package_path_from_env() {
1456 let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
1457
1458 temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
1459 let (conf, err) =
1460 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1461 assert!(err.is_none());
1462 let applied_cache_path = conf
1463 .typst_extra_args
1464 .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
1465 assert!(applied_cache_path);
1466 });
1467 }
1468
1469 #[test]
1470 #[cfg(feature = "preview")]
1471 fn test_invert_colors_validation() {
1472 fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
1473 Ok(serde_json::from_str(s)?)
1474 }
1475
1476 assert_eq!(
1477 test(r#""never""#).unwrap(),
1478 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1479 );
1480 assert_eq!(
1481 test(r#""auto""#).unwrap(),
1482 PreviewInvertColors::Enum(PreviewInvertColor::Auto)
1483 );
1484 assert_eq!(
1485 test(r#""always""#).unwrap(),
1486 PreviewInvertColors::Enum(PreviewInvertColor::Always)
1487 );
1488 assert!(test(r#""e""#).is_err());
1489
1490 assert_eq!(
1491 test(r#"{"rest": "never"}"#).unwrap(),
1492 PreviewInvertColors::Object(PreviewInvertColorObject {
1493 image: PreviewInvertColor::Never,
1494 rest: PreviewInvertColor::Never,
1495 })
1496 );
1497 assert_eq!(
1498 test(r#"{"image": "always"}"#).unwrap(),
1499 PreviewInvertColors::Object(PreviewInvertColorObject {
1500 image: PreviewInvertColor::Always,
1501 rest: PreviewInvertColor::Never,
1502 })
1503 );
1504 assert_eq!(
1505 test(r#"{}"#).unwrap(),
1506 PreviewInvertColors::Object(PreviewInvertColorObject {
1507 image: PreviewInvertColor::Never,
1508 rest: PreviewInvertColor::Never,
1509 })
1510 );
1511 assert_eq!(
1512 test(r#"{"unknown": "ovo"}"#).unwrap(),
1513 PreviewInvertColors::Object(PreviewInvertColorObject {
1514 image: PreviewInvertColor::Never,
1515 rest: PreviewInvertColor::Never,
1516 })
1517 );
1518 assert!(test(r#"{"image": "e"}"#).is_err());
1519 }
1520}