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 "development",
50 "exportPdf",
51 "exportTarget",
52 "fontPaths",
53 "formatterMode",
54 "formatterPrintWidth",
55 "formatterIndentSize",
56 "formatterProseWrap",
57 "hoverPeriscope",
58 "onEnter",
59 "outputPath",
60 "syntaxOnly",
61 "preview",
62 "projectResolution",
63 "rootPath",
64 "semanticTokens",
65 "systemFonts",
66 "typstExtraArgs",
67];
68#[derive(Debug, Default, Clone)]
76pub struct Config {
77 pub const_config: ConstConfig,
79 pub const_dap_config: ConstDapConfig,
81
82 pub delegate_fs_requests: bool,
84 pub customized_show_document: bool,
86 pub has_default_entry_path: bool,
88 pub notify_status: bool,
90 pub support_html_in_markdown: bool,
92 pub extended_code_action: bool,
95 pub development: bool,
97 pub syntax_only: bool,
99
100 pub color_theme: Option<String>,
102 pub entry_resolver: EntryResolver,
104 pub lsp_inputs: ImmutDict,
106 pub periscope_args: Option<PeriscopeArgs>,
108 pub typst_extra_args: Option<TypstExtraArgs>,
110 pub semantic_tokens: SemanticTokensMode,
112
113 pub completion: CompletionFeat,
115 pub preview: PreviewFeat,
117 pub lint: LintFeat,
119 pub on_enter: OnEnterFeat,
121
122 pub font_opts: CompileFontArgs,
124 pub font_paths: Vec<PathBuf>,
126 pub fonts: OnceLock<Derived<Arc<FontResolverImpl>>>,
128 pub system_fonts: Option<bool>,
130
131 pub watch_access_model: OnceLock<Derived<Arc<WatchAccessModel>>>,
133 pub access_model: OnceLock<Derived<Arc<dyn LspAccessModel>>>,
135
136 pub export_target: ExportTarget,
138 pub export_pdf: TaskWhen,
140 pub output_path: PathPattern,
142
143 pub formatter_mode: FormatterMode,
145 pub formatter_print_width: Option<u32>,
148 pub formatter_indent_size: Option<u32>,
150 pub formatter_prose_wrap: Option<bool>,
152 pub warnings: Vec<CowStr>,
154}
155
156impl Config {
157 pub fn new(
159 const_config: ConstConfig,
160 roots: Vec<ImmutPath>,
161 font_opts: CompileFontArgs,
162 ) -> Self {
163 let mut config = Self {
164 const_config,
165 const_dap_config: ConstDapConfig::default(),
166 entry_resolver: EntryResolver {
167 roots,
168 ..EntryResolver::default()
169 },
170 font_opts,
171 ..Self::default()
172 };
173 config
174 .update_by_map(&Map::default())
175 .log_error("failed to assign Config defaults");
176 config
177 }
178
179 pub fn extract_lsp_params(
185 params: InitializeParams,
186 font_args: CompileFontArgs,
187 ) -> (Self, Option<ResponseError>) {
188 let roots = match params.workspace_folders.as_ref() {
190 Some(roots) => roots
191 .iter()
192 .map(|root| ImmutPath::from(url_to_path(&root.uri)))
193 .collect(),
194 #[allow(deprecated)] None => params
196 .root_uri
197 .as_ref()
198 .map(|uri| ImmutPath::from(url_to_path(uri)))
199 .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into()))
200 .into_iter()
201 .collect(),
202 };
203 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
204
205 if let Some(locale) = config.const_config.locale.as_ref() {
207 tinymist_l10n::set_locale(locale);
208 }
209 config.configure_syntax_only();
210
211 let err = params
212 .initialization_options
213 .and_then(|init| config.update(&init).map_err(invalid_params).err());
214
215 (config, err)
216 }
217
218 pub fn extract_dap_params(
224 params: dapts::InitializeRequestArguments,
225 font_args: CompileFontArgs,
226 ) -> (Self, Option<ResponseError>) {
227 let cwd = std::env::current_dir()
229 .expect("failed to get current directory")
230 .into();
231
232 let roots = vec![cwd];
234 let mut config = Config::new(ConstConfig::from(¶ms), roots, font_args);
235 config.const_dap_config = ConstDapConfig::from(¶ms);
236
237 if let Some(locale) = config.const_config.locale.as_ref() {
239 tinymist_l10n::set_locale(locale);
240 }
241
242 (config, None)
243 }
244
245 pub fn get_items() -> Vec<ConfigurationItem> {
248 CONFIG_ITEMS
249 .iter()
250 .flat_map(|&item| [format!("tinymist.{item}"), item.to_owned()])
251 .map(|section| ConfigurationItem {
252 section: Some(section),
253 ..ConfigurationItem::default()
254 })
255 .collect()
256 }
257
258 pub fn values_to_map(values: Vec<JsonValue>) -> Map<String, JsonValue> {
260 let unpaired_values = values
261 .into_iter()
262 .tuples()
263 .map(|(a, b)| if !a.is_null() { a } else { b });
264
265 CONFIG_ITEMS
266 .iter()
267 .map(|&item| item.to_owned())
268 .zip(unpaired_values)
269 .collect()
270 }
271
272 pub fn update(&mut self, update: &JsonValue) -> Result<()> {
277 if let JsonValue::Object(update) = update {
278 self.update_by_map(update)?;
279
280 if let Some(namespaced) = update.get("tinymist").and_then(JsonValue::as_object) {
282 self.update_by_map(namespaced)?;
283 }
284
285 Ok(())
286 } else {
287 tinymist_l10n::bail!(
288 "tinymist.config.invalidObject",
289 "invalid configuration object: {object}",
290 object = update.debug_l10n(),
291 )
292 }
293 }
294
295 pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> Result<()> {
300 log::info!(
301 "ServerState: config update_by_map {}",
302 serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
303 );
304
305 self.warnings.clear();
306
307 macro_rules! try_deserialize {
308 ($ty:ty, $key:expr) => {
309 update.get($key).and_then(|v| {
310 <$ty>::deserialize(v)
311 .inspect_err(|err| {
312 if v.is_null() {
315 return;
316 }
317
318 self.warnings.push(tinymist_l10n::t!(
319 "tinymist.config.deserializeError",
320 "failed to deserialize \"{key}\": {err}",
321 key = $key.debug_l10n(),
322 err = err.debug_l10n(),
323 ));
324 })
325 .ok()
326 })
327 };
328 }
329
330 macro_rules! assign_config {
331 ($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
332 let v = try_deserialize!($ty, $bind);
333 self.$($field_path).+ = v.unwrap_or_default();
334 };
335 ($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
336 let v = try_deserialize!($ty, $bind);
337 self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
338 };
339 }
340
341 assign_config!(color_theme := "colorTheme"?: Option<String>);
342 assign_config!(lint := "lint"?: LintFeat);
343 assign_config!(completion := "completion"?: CompletionFeat);
344 assign_config!(on_enter := "onEnter"?: OnEnterFeat);
345 assign_config!(completion.trigger_suggest := "triggerSuggest"?: bool);
346 assign_config!(completion.trigger_parameter_hints := "triggerParameterHints"?: bool);
347 assign_config!(completion.trigger_suggest_and_parameter_hints := "triggerSuggestAndParameterHints"?: bool);
348 assign_config!(customized_show_document := "customizedShowDocument"?: bool);
349 assign_config!(entry_resolver.project_resolution := "projectResolution"?: ProjectResolutionKind);
350 assign_config!(export_pdf := "exportPdf"?: TaskWhen);
351 assign_config!(export_target := "exportTarget"?: ExportTarget);
352 assign_config!(font_paths := "fontPaths"?: Vec<_>);
353 assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
354 assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
355 assign_config!(formatter_indent_size := "formatterIndentSize"?: Option<u32>);
356 assign_config!(formatter_prose_wrap := "formatterProseWrap"?: Option<bool>);
357 assign_config!(output_path := "outputPath"?: PathPattern);
358 assign_config!(preview := "preview"?: PreviewFeat);
359 assign_config!(lint := "lint"?: LintFeat);
360 assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
361 assign_config!(delegate_fs_requests := "delegateFsRequests"?: bool);
362 assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
363 assign_config!(extended_code_action := "supportExtendedCodeAction"?: bool);
364 assign_config!(development := "development"?: bool);
365 assign_config!(system_fonts := "systemFonts"?: Option<bool>);
366
367 self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
368 Some("enable") => true,
369 Some("disable") | None => false,
370 Some(value) => {
371 self.warnings.push(tinymist_l10n::t!(
372 "tinymist.config.badCompileStatus",
373 "compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
374 value = value.debug_l10n(),
375 ));
376
377 false
378 }
379 };
380 self.syntax_only = match try_(|| update.get("syntaxOnly")?.as_str()) {
381 Some("onPowerSaving") => tinymist_std::battery::is_power_saving(),
382 Some("enable") => true,
383 Some("disable" | "auto") | None => false,
384 Some(value) => {
385 self.warnings.push(tinymist_l10n::t!(
386 "tinymist.config.badSyntaxOnly",
387 "syntaxOnly must be either `\"enable\"`, `\"disable\", `\"onPowerSaving\"`, or `\"auto\"`, got {value}",
388 value = value.debug_l10n(),
389 ));
390
391 false
392 }
393 };
394
395 self.periscope_args = match update.get("hoverPeriscope") {
397 Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()),
398 Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None,
399 Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
400 Ok(args) => Some(args),
401 Err(err) => {
402 self.warnings.push(tinymist_l10n::t!(
403 "tinymist.config.badHoverPeriscope",
404 "failed to parse hoverPeriscope: {err}",
405 err = err.debug_l10n(),
406 ));
407 None
408 }
409 },
410 };
411 if let Some(args) = self.periscope_args.as_mut() {
412 if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") {
413 "always".clone_into(&mut args.invert_color);
414 }
415 }
416
417 fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> CowStr {
418 log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
419 tinymist_l10n::t!(
420 "tinymist.config.badTypstExtraArgs",
421 "failed to parse typstExtraArgs: {err}, args: {args}",
422 err = err.debug_l10n(),
423 args = args.debug_l10n(),
424 )
425 }
426
427 {
428 let raw_args = || update.get("typstExtraArgs");
429 let typst_args: Vec<String> = match raw_args().cloned().map(serde_json::from_value) {
430 Some(Ok(args)) => args,
431 Some(Err(err)) => {
432 self.warnings.push(invalid_extra_args(&raw_args(), err));
433 None
434 }
435 None => None,
438 }
439 .unwrap_or_default();
440 let empty_typst_args = typst_args.is_empty();
441
442 let args = match CompileOnceArgs::try_parse_from(
443 Some("typst-cli".to_owned()).into_iter().chain(typst_args),
444 ) {
445 Ok(args) => args,
446 Err(err) => {
447 self.warnings.push(invalid_extra_args(&raw_args(), err));
448
449 if empty_typst_args {
450 CompileOnceArgs::default()
451 } else {
452 CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
454 .inspect_err(|err| {
455 log::error!("failed to make default typstExtraArgs: {err}");
456 })
457 .unwrap_or_default()
458 }
459 }
460 };
461
462 self.typst_extra_args = Some(TypstExtraArgs {
464 inputs: args.resolve_inputs().unwrap_or_default(),
465 entry: args.input.map(|e| Path::new(&e).into()),
466 root_dir: args.root.as_ref().map(|r| r.as_path().into()),
467 font: args.font,
468 package: args.package,
469 pdf_standard: args.pdf.standard,
470 no_pdf_tags: args.pdf.no_tags,
471 ppi: args.png.ppi,
472 features: args.features,
473 creation_timestamp: args.creation_timestamp,
474 cert: args.cert.as_deref().map(From::from),
475 });
476 }
477
478 self.entry_resolver.root_path =
479 try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
480 self.typst_extra_args
481 .as_ref()
482 .and_then(|e| e.root_dir.clone())
483 });
484 self.entry_resolver.entry = self.typst_extra_args.as_ref().and_then(|e| e.entry.clone());
485 self.has_default_entry_path = self.entry_resolver.resolve_default().is_some();
486 self.lsp_inputs = {
487 let mut dict = TypstDict::default();
488
489 #[derive(Serialize)]
490 #[serde(rename_all = "camelCase")]
491 struct PreviewInputs {
492 pub version: u32,
493 pub theme: String,
494 }
495
496 dict.insert(
497 "x-preview".into(),
498 serde_json::to_string(&PreviewInputs {
499 version: 1,
500 theme: self.color_theme.clone().unwrap_or_default(),
501 })
502 .unwrap()
503 .into_value(),
504 );
505
506 Arc::new(LazyHash::new(dict))
507 };
508
509 self.validate()
510 }
511
512 pub fn validate(&self) -> Result<()> {
514 self.entry_resolver.validate()?;
515
516 Ok(())
517 }
518
519 pub fn configure_syntax_only(&self) {
521 if self.syntax_only {
522 log::info!("Server: running lsp in syntax-only mode, some features may be disabled");
523 SYNTAX_ONLY.store(true, std::sync::atomic::Ordering::SeqCst);
524 } else {
525 log::info!("Server: running lsp in full mode");
526 SYNTAX_ONLY.store(false, std::sync::atomic::Ordering::SeqCst);
527 }
528 }
529
530 pub fn formatter(&self) -> FormatUserConfig {
532 let formatter_print_width = self.formatter_print_width.unwrap_or(120) as usize;
533 let formatter_indent_size = self.formatter_indent_size.unwrap_or(2) as usize;
534 let formatter_line_wrap = self.formatter_prose_wrap.unwrap_or(false);
535
536 FormatUserConfig {
537 config: match self.formatter_mode {
538 FormatterMode::Typstyle => {
539 FormatterConfig::Typstyle(Box::new(typstyle_core::Config {
540 tab_spaces: formatter_indent_size,
541 max_width: formatter_print_width,
542 wrap_text: formatter_line_wrap,
543 ..typstyle_core::Config::default()
544 }))
545 }
546 FormatterMode::Typstfmt => FormatterConfig::Typstfmt(Box::new(typstfmt::Config {
547 max_line_length: formatter_print_width,
548 indent_space: formatter_indent_size,
549 line_wrap: formatter_line_wrap,
550 ..typstfmt::Config::default()
551 })),
552 FormatterMode::Disable => FormatterConfig::Disable,
553 },
554 position_encoding: self.const_config.position_encoding,
555 }
556 }
557
558 #[cfg(feature = "preview")]
560 pub fn preview(&self) -> PreviewConfig {
561 PreviewConfig {
562 enable_partial_rendering: self.preview.partial_rendering,
563 refresh_style: self.preview.refresh.clone().unwrap_or(TaskWhen::OnType),
564 invert_colors: serde_json::to_string(&self.preview.invert_colors)
565 .unwrap_or_else(|_| "never".to_string()),
566 }
567 }
568
569 pub(crate) fn export_task(&self) -> ExportTask {
571 ExportTask {
572 when: self.export_pdf.clone(),
573 output: Some(self.output_path.clone()),
574 transform: vec![],
575 }
576 }
577
578 #[cfg(feature = "export")]
580 pub(crate) fn export(&self) -> ExportUserConfig {
581 let export = self.export_task();
582 ExportUserConfig {
583 export_target: self.export_target,
584 task: ProjectTask::ExportPdf(ExportPdfTask {
594 export,
595 pages: None, pdf_standards: self.pdf_standards().unwrap_or_default(),
597 no_pdf_tags: self.no_pdf_tags(),
598 creation_timestamp: self.creation_timestamp(),
599 }),
600 count_words: self.notify_status,
601 development: self.development,
602 }
603 }
604
605 pub fn font_opts(&self) -> CompileFontArgs {
607 let mut opts = self.font_opts.clone();
608
609 if let Some(system_fonts) = self.system_fonts.or_else(|| {
610 self.typst_extra_args
611 .as_ref()
612 .map(|x| !x.font.ignore_system_fonts)
613 }) {
614 opts.ignore_system_fonts = !system_fonts;
615 }
616
617 let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
618 let font_paths =
619 font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths));
620 if let Some(paths) = font_paths {
621 opts.font_paths.clone_from(paths);
622 }
623
624 let root = OnceLock::new();
625 for path in opts.font_paths.iter_mut() {
626 if path.is_relative() {
627 if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) {
628 let p = std::mem::take(path);
629 *path = root.join(p);
630 }
631 }
632 }
633
634 opts
635 }
636
637 pub fn package_opts(&self) -> CompilePackageArgs {
639 if let Some(extras) = &self.typst_extra_args {
640 return extras.package.clone();
641 }
642 CompilePackageArgs::default()
643 }
644
645 pub fn fonts(&self) -> Arc<FontResolverImpl> {
647 let font = || {
649 let opts = self.font_opts();
650
651 log::info!("creating SharedFontResolver with {opts:?}");
652 Derived(
653 crate::project::LspUniverseBuilder::resolve_fonts(opts)
654 .map(Arc::new)
655 .expect("failed to create font book"),
656 )
657 };
658 self.fonts.get_or_init(font).clone().0
659 }
660
661 pub fn inputs(&self) -> ImmutDict {
663 #[comemo::memoize]
664 fn combine(lhs: ImmutDict, rhs: ImmutDict) -> ImmutDict {
665 let mut dict = (**lhs).clone();
666 for (k, v) in rhs.iter() {
667 dict.insert(k.clone(), v.clone());
668 }
669
670 Arc::new(LazyHash::new(dict))
671 }
672
673 combine(self.user_inputs(), self.lsp_inputs.clone())
674 }
675
676 fn user_inputs(&self) -> ImmutDict {
677 static EMPTY: LazyLock<ImmutDict> = LazyLock::new(ImmutDict::default);
678
679 if let Some(extras) = &self.typst_extra_args {
680 return extras.inputs.clone();
681 }
682
683 EMPTY.clone()
684 }
685
686 pub fn typst_features(&self) -> Option<Features> {
688 let features = &self.typst_extra_args.as_ref()?.features;
689 Some(Features::from_iter(features.iter().map(|f| (*f).into())))
690 }
691
692 pub fn pdf_standards(&self) -> Option<Vec<PdfStandard>> {
694 Some(self.typst_extra_args.as_ref()?.pdf_standard.clone())
695 }
696
697 pub fn no_pdf_tags(&self) -> bool {
699 self.typst_extra_args
700 .as_ref()
701 .is_some_and(|x| x.no_pdf_tags)
702 }
703
704 pub fn ppi(&self) -> Option<f32> {
706 Some(self.typst_extra_args.as_ref()?.ppi)
707 }
708
709 pub fn creation_timestamp(&self) -> Option<i64> {
711 self.typst_extra_args.as_ref()?.creation_timestamp
712 }
713
714 pub fn certification_path(&self) -> Option<ImmutPath> {
716 self.typst_extra_args.as_ref()?.cert.clone()
717 }
718
719 #[allow(clippy::type_complexity)]
721 pub fn primary_opts(
722 &self,
723 ) -> (
724 bool,
725 ImmutDict,
726 ExportTarget,
727 Option<Vec<typst::Feature>>,
728 Option<ImmutPath>,
729 CompilePackageArgs,
730 Option<bool>,
731 CompileFontArgs,
732 Option<i64>,
733 Option<Arc<Path>>,
734 ) {
735 (
736 self.syntax_only,
738 self.user_inputs(),
740 self.export_target,
741 self.typst_features().map(|feat| {
742 let mut features = vec![];
743 if feat.is_enabled(typst::Feature::Html) {
744 features.push(typst::Feature::Html);
745 }
746 if feat.is_enabled(typst::Feature::A11yExtras) {
747 features.push(typst::Feature::A11yExtras);
748 }
749
750 features
751 }),
752 self.certification_path(),
754 self.package_opts(),
755 self.system_fonts,
757 self.font_opts(),
758 self.creation_timestamp(),
759 self.entry_resolver
761 .root(self.entry_resolver.resolve_default().as_ref()),
762 )
763 }
764
765 #[cfg(not(feature = "system"))]
766 fn create_physical_access_model(
767 &self,
768 client: &TypedLspClient<ServerState>,
769 ) -> Arc<dyn LspAccessModel> {
770 self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>
771 }
772
773 #[cfg(feature = "system")]
774 fn create_physical_access_model(
775 &self,
776 _client: &TypedLspClient<ServerState>,
777 ) -> Arc<dyn LspAccessModel> {
778 use reflexo_typst::vfs::system::SystemAccessModel;
779 Arc::new(SystemAccessModel {})
780 }
781
782 pub(crate) fn watch_access_model(
783 &self,
784 client: &TypedLspClient<ServerState>,
785 ) -> &Arc<WatchAccessModel> {
786 let client = client.clone();
787 &self
788 .watch_access_model
789 .get_or_init(|| Derived(Arc::new(WatchAccessModel::new(client))))
790 .0
791 }
792
793 pub(crate) fn access_model(&self, client: &TypedLspClient<ServerState>) -> DynAccessModel {
794 let access_model = || {
795 log::info!(
796 "creating AccessModel with delegation={:?}",
797 self.delegate_fs_requests
798 );
799 if self.delegate_fs_requests {
800 Derived(self.watch_access_model(client).clone() as Arc<dyn LspAccessModel>)
801 } else {
802 Derived(self.create_physical_access_model(client))
803 }
804 };
805 DynAccessModel(self.access_model.get_or_init(access_model).0.clone())
806 }
807}
808
809#[derive(Debug, Clone)]
812pub struct ConstConfig {
813 pub position_encoding: PositionEncoding,
816 pub cfg_change_registration: bool,
818 pub notify_will_rename_files: bool,
820 pub tokens_dynamic_registration: bool,
822 pub tokens_overlapping_token_support: bool,
824 pub tokens_multiline_token_support: bool,
826 pub doc_line_folding_only: bool,
828 pub doc_fmt_dynamic_registration: bool,
830 pub locale: Option<String>,
832}
833
834impl Default for ConstConfig {
835 fn default() -> Self {
836 Self::from(&InitializeParams::default())
837 }
838}
839
840impl From<&InitializeParams> for ConstConfig {
841 fn from(params: &InitializeParams) -> Self {
842 let position_encoding = {
847 PositionEncoding::Utf16
858 };
859
860 let workspace = params.capabilities.workspace.as_ref();
861 let file_operations = try_(|| workspace?.file_operations.as_ref());
862 let doc = params.capabilities.text_document.as_ref();
863 let sema = try_(|| doc?.semantic_tokens.as_ref());
864 let fold = try_(|| doc?.folding_range.as_ref());
865 let format = try_(|| doc?.formatting.as_ref());
866
867 let locale = params
868 .initialization_options
869 .as_ref()
870 .and_then(|init| init.get("locale").and_then(|v| v.as_str()))
871 .or(params.locale.as_deref());
872
873 Self {
874 position_encoding,
875 cfg_change_registration: try_or(|| workspace?.configuration, false),
876 notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
877 tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
878 tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
879 tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),
880 doc_line_folding_only: try_or(|| fold?.line_folding_only, true),
881 doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false),
882 locale: locale.map(ToOwned::to_owned),
883 }
884 }
885}
886
887impl From<&dapts::InitializeRequestArguments> for ConstConfig {
888 fn from(params: &dapts::InitializeRequestArguments) -> Self {
889 let locale = params.locale.as_deref();
890
891 Self {
892 locale: locale.map(ToOwned::to_owned),
893 ..Default::default()
894 }
895 }
896}
897
898pub type DapPathFormat = dapts::InitializeRequestArgumentsPathFormat;
901
902#[derive(Debug, Clone)]
905pub struct ConstDapConfig {
906 pub path_format: DapPathFormat,
908 pub lines_start_at1: bool,
910 pub columns_start_at1: bool,
912}
913
914impl Default for ConstDapConfig {
915 fn default() -> Self {
916 Self::from(&dapts::InitializeRequestArguments::default())
917 }
918}
919
920impl From<&dapts::InitializeRequestArguments> for ConstDapConfig {
921 fn from(params: &dapts::InitializeRequestArguments) -> Self {
922 Self {
923 path_format: params.path_format.clone().unwrap_or(DapPathFormat::Path),
924 lines_start_at1: params.lines_start_at1.unwrap_or(true),
925 columns_start_at1: params.columns_start_at1.unwrap_or(true),
926 }
927 }
928}
929
930#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub enum FormatterMode {
934 #[default]
936 Disable,
937 Typstyle,
939 Typstfmt,
941}
942
943#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
945#[serde(rename_all = "camelCase")]
946pub enum SemanticTokensMode {
947 Disable,
949 #[default]
951 Enable,
952}
953
954#[derive(Debug, Default, Clone, Deserialize)]
956#[serde(rename_all = "camelCase")]
957pub struct PreviewFeat {
958 #[serde(default, deserialize_with = "deserialize_null_default")]
960 pub browsing: BrowsingPreviewOpts,
961 #[serde(default, deserialize_with = "deserialize_null_default")]
963 pub background: BackgroundPreviewOpts,
964 #[serde(default)]
966 pub refresh: Option<TaskWhen>,
967 #[serde(default, deserialize_with = "deserialize_null_default")]
969 pub partial_rendering: bool,
970 #[cfg(feature = "preview")]
972 #[serde(default, deserialize_with = "deserialize_null_default")]
973 pub invert_colors: PreviewInvertColors,
974}
975
976#[derive(Debug, Default, Clone, Deserialize)]
978pub struct LintFeat {
979 pub enabled: Option<bool>,
981 pub when: Option<TaskWhen>,
983}
984
985impl LintFeat {
986 pub fn when(&self) -> &TaskWhen {
988 if matches!(self.enabled, Some(false) | None) {
989 return &TaskWhen::Never;
990 }
991
992 self.when.as_ref().unwrap_or(&TaskWhen::OnSave)
993 }
994}
995#[derive(Debug, Default, Clone, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct OnEnterFeat {
999 #[serde(default, deserialize_with = "deserialize_null_default")]
1001 pub handle_list: bool,
1002}
1003
1004#[derive(Debug, Default, Clone, Deserialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct BrowsingPreviewOpts {
1008 pub args: Option<Vec<String>>,
1010}
1011
1012#[derive(Debug, Default, Clone, Deserialize)]
1014#[serde(rename_all = "camelCase")]
1015pub struct BackgroundPreviewOpts {
1016 #[serde(default, deserialize_with = "deserialize_null_default")]
1018 pub enabled: bool,
1019 pub args: Option<Vec<String>>,
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Default)]
1027pub struct TypstExtraArgs {
1028 pub root_dir: Option<ImmutPath>,
1030 pub entry: Option<ImmutPath>,
1032 pub inputs: ImmutDict,
1034 pub font: CompileFontArgs,
1036 pub package: CompilePackageArgs,
1038 pub features: Vec<Feature>,
1041 pub pdf_standard: Vec<PdfStandard>,
1044 pub ppi: f32,
1046 pub no_pdf_tags: bool,
1051 pub creation_timestamp: Option<i64>,
1053 pub cert: Option<ImmutPath>,
1055}
1056
1057pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
1058 SemanticTokensOptions {
1059 legend: SemanticTokensLegend {
1060 token_types: TokenType::iter()
1061 .filter(|e| *e != TokenType::None)
1062 .map(Into::into)
1063 .collect(),
1064 token_modifiers: Modifier::iter().map(Into::into).collect(),
1065 },
1066 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
1067 ..SemanticTokensOptions::default()
1068 }
1069}
1070
1071fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1072where
1073 T: Default + Deserialize<'de>,
1074 D: serde::Deserializer<'de>,
1075{
1076 let opt = Option::deserialize(deserializer)?;
1077 Ok(opt.unwrap_or_default())
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use serde_json::json;
1084 #[cfg(feature = "preview")]
1085 use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
1086
1087 fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
1088 temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
1089 }
1090
1091 fn good_config(config: &mut Config, update: &JsonValue) {
1092 update_config(config, update).expect("not good");
1093 assert!(config.warnings.is_empty(), "{:?}", config.warnings);
1094 }
1095
1096 #[test]
1097 fn test_default_encoding() {
1098 let cc = ConstConfig::default();
1099 assert_eq!(cc.position_encoding, PositionEncoding::Utf16);
1100 }
1101
1102 #[test]
1103 fn test_config_update() {
1104 let mut config = Config::default();
1105
1106 let root_path = Path::new(if cfg!(windows) {
1107 "C:\\dummy-root"
1108 } else {
1109 "/dummy-root"
1110 });
1111
1112 let update = json!({
1113 "outputPath": "out",
1114 "exportPdf": "onSave",
1115 "rootPath": root_path,
1116 "semanticTokens": "enable",
1117 "formatterMode": "typstyle",
1118 "typstExtraArgs": ["--root", root_path]
1119 });
1120
1121 good_config(&mut config, &update);
1122
1123 let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
1125 if has_source_date_epoch {
1126 let args = config.typst_extra_args.as_mut().unwrap();
1127 assert!(args.creation_timestamp.is_some());
1128 args.creation_timestamp = None;
1129 }
1130
1131 assert_eq!(config.output_path, PathPattern::new("out"));
1132 assert_eq!(config.export_pdf, TaskWhen::OnSave);
1133 assert_eq!(
1134 config.entry_resolver.root_path,
1135 Some(ImmutPath::from(root_path))
1136 );
1137 assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
1138 assert_eq!(config.formatter_mode, FormatterMode::Typstyle);
1139 assert_eq!(
1140 config.typst_extra_args,
1141 Some(TypstExtraArgs {
1142 root_dir: Some(ImmutPath::from(root_path)),
1143 ppi: 144.0,
1144 ..TypstExtraArgs::default()
1145 })
1146 );
1147 }
1148
1149 #[test]
1150 fn test_namespaced_config() {
1151 let mut config = Config::default();
1152
1153 let update = json!({
1155 "exportPdf": "onSave",
1156 "tinymist": {
1157 "exportPdf": "onType",
1158 }
1159 });
1160
1161 good_config(&mut config, &update);
1162
1163 assert_eq!(config.export_pdf, TaskWhen::OnType);
1164 }
1165
1166 #[test]
1167 fn test_compile_status() {
1168 let mut config = Config::default();
1169
1170 let update = json!({
1171 "compileStatus": "enable",
1172 });
1173 good_config(&mut config, &update);
1174 assert!(config.notify_status);
1175
1176 let update = json!({
1177 "compileStatus": "disable",
1178 });
1179 good_config(&mut config, &update);
1180 assert!(!config.notify_status);
1181 }
1182
1183 #[test]
1184 fn test_config_creation_timestamp() {
1185 type Timestamp = Option<i64>;
1186
1187 fn timestamp(f: impl FnOnce(&mut Config)) -> Timestamp {
1188 let mut config = Config::default();
1189
1190 f(&mut config);
1191
1192 let args = config.typst_extra_args;
1193 args.and_then(|args| args.creation_timestamp)
1194 }
1195
1196 let args_timestamp = timestamp(|config| {
1204 let update = json!({
1205 "typstExtraArgs": ["--creation-timestamp", "1234"]
1206 });
1207 good_config(config, &update);
1208 });
1209 assert!(args_timestamp.is_some());
1210
1211 }
1219
1220 #[test]
1221 fn test_empty_extra_args() {
1222 let mut config = Config::default();
1223 let update = json!({
1224 "typstExtraArgs": []
1225 });
1226
1227 good_config(&mut config, &update);
1228 }
1229
1230 #[test]
1231 fn test_null_args() {
1232 fn test_good_config(path: &str) -> Config {
1233 let mut obj = json!(null);
1234 let path = path.split('.').collect::<Vec<_>>();
1235 for p in path.iter().rev() {
1236 obj = json!({ *p: obj });
1237 }
1238
1239 let mut c = Config::default();
1240 good_config(&mut c, &obj);
1241 c
1242 }
1243
1244 test_good_config("root");
1245 test_good_config("rootPath");
1246 test_good_config("colorTheme");
1247 test_good_config("lint");
1248 test_good_config("customizedShowDocument");
1249 test_good_config("projectResolution");
1250 test_good_config("exportPdf");
1251 test_good_config("exportTarget");
1252 test_good_config("fontPaths");
1253 test_good_config("formatterMode");
1254 test_good_config("formatterPrintWidth");
1255 test_good_config("formatterIndentSize");
1256 test_good_config("formatterProseWrap");
1257 test_good_config("outputPath");
1258 test_good_config("semanticTokens");
1259 test_good_config("delegateFsRequests");
1260 test_good_config("supportHtmlInMarkdown");
1261 test_good_config("supportExtendedCodeAction");
1262 test_good_config("development");
1263 test_good_config("systemFonts");
1264
1265 test_good_config("completion");
1266 test_good_config("completion.triggerSuggest");
1267 test_good_config("completion.triggerParameterHints");
1268 test_good_config("completion.triggerSuggestAndParameterHints");
1269 test_good_config("completion.triggerOnSnippetPlaceholders");
1270 test_good_config("completion.symbol");
1271 test_good_config("completion.postfix");
1272 test_good_config("completion.postfixUfcs");
1273 test_good_config("completion.postfixUfcsLeft");
1274 test_good_config("completion.postfixUfcsRight");
1275 test_good_config("completion.postfixSnippets");
1276
1277 test_good_config("lint");
1278 test_good_config("lint.enabled");
1279 test_good_config("lint.when");
1280
1281 test_good_config("preview");
1282 test_good_config("preview.browsing");
1283 test_good_config("preview.browsing.args");
1284 test_good_config("preview.background");
1285 test_good_config("preview.background.enabled");
1286 test_good_config("preview.background.args");
1287 test_good_config("preview.refresh");
1288 test_good_config("preview.partialRendering");
1289 #[cfg(feature = "preview")]
1290 let c = test_good_config("preview.invertColors");
1291 #[cfg(feature = "preview")]
1292 assert_eq!(
1293 c.preview.invert_colors,
1294 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1295 );
1296 }
1297
1298 #[test]
1299 fn test_font_opts() {
1300 fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
1301 let mut config = Config::default();
1302 if let Some(update) = update {
1303 good_config(&mut config, update);
1304 }
1305
1306 config.font_opts()
1307 }
1308
1309 let font_opts = opts(None);
1310 assert!(!font_opts.ignore_system_fonts);
1311
1312 let font_opts = opts(Some(&json!({})));
1313 assert!(!font_opts.ignore_system_fonts);
1314
1315 let font_opts = opts(Some(&json!({
1316 "typstExtraArgs": []
1317 })));
1318 assert!(!font_opts.ignore_system_fonts);
1319
1320 let font_opts = opts(Some(&json!({
1321 "systemFonts": false,
1322 })));
1323 assert!(font_opts.ignore_system_fonts);
1324
1325 let font_opts = opts(Some(&json!({
1326 "typstExtraArgs": ["--ignore-system-fonts"]
1327 })));
1328 assert!(font_opts.ignore_system_fonts);
1329
1330 let font_opts = opts(Some(&json!({
1331 "systemFonts": true,
1332 "typstExtraArgs": ["--ignore-system-fonts"]
1333 })));
1334 assert!(!font_opts.ignore_system_fonts);
1335 }
1336
1337 #[test]
1338 fn test_preview_opts() {
1339 fn opts(update: Option<&JsonValue>) -> PreviewFeat {
1340 let mut config = Config::default();
1341 if let Some(update) = update {
1342 good_config(&mut config, update);
1343 }
1344
1345 config.preview
1346 }
1347
1348 let preview = opts(Some(&json!({
1349 "preview": {
1350 }
1351 })));
1352 assert_eq!(preview.refresh, None);
1353
1354 let preview = opts(Some(&json!({
1355 "preview": {
1356 "refresh":"onType"
1357 }
1358 })));
1359 assert_eq!(preview.refresh, Some(TaskWhen::OnType));
1360
1361 let preview = opts(Some(&json!({
1362 "preview": {
1363 "refresh":"onSave"
1364 }
1365 })));
1366 assert_eq!(preview.refresh, Some(TaskWhen::OnSave));
1367 }
1368
1369 #[test]
1370 fn test_reject_abnormal_root() {
1371 let mut config = Config::default();
1372 let update = json!({
1373 "rootPath": ".",
1374 });
1375
1376 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1377 assert!(err.contains("absolute path"), "unexpected error: {err}");
1378 }
1379
1380 #[test]
1381 fn test_reject_abnormal_root2() {
1382 let mut config = Config::default();
1383 let update = json!({
1384 "typstExtraArgs": ["--root", "."]
1385 });
1386
1387 let err = format!("{}", update_config(&mut config, &update).unwrap_err());
1388 assert!(err.contains("absolute path"), "unexpected error: {err}");
1389 }
1390
1391 #[test]
1392 fn test_entry_by_extra_args() {
1393 let simple_config = {
1394 let mut config = Config::default();
1395 let update = json!({
1396 "typstExtraArgs": ["main.typ"]
1397 });
1398
1399 update_config(&mut config, &update).expect("updated");
1401 update_config(&mut config, &update).expect("updated");
1403 config
1404 };
1405 {
1406 let mut config = Config::default();
1407 let update = json!({
1408 "typstExtraArgs": ["main.typ", "main.typ"]
1409 });
1410 update_config(&mut config, &update).unwrap();
1411 let warns = format!("{:?}", config.warnings);
1412 assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
1413 assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
1414 }
1415 {
1416 let mut config = Config::default();
1417 let update = json!({
1418 "typstExtraArgs": ["main2.typ"],
1419 "tinymist": {
1420 "typstExtraArgs": ["main.typ"]
1421 }
1422 });
1423
1424 update_config(&mut config, &update).expect("updated");
1426 update_config(&mut config, &update).expect("updated");
1428
1429 assert_eq!(config.typst_extra_args, simple_config.typst_extra_args);
1430 }
1431 }
1432
1433 #[test]
1434 fn test_default_formatting_config() {
1435 let config = Config::default().formatter();
1436 assert!(matches!(config.config, FormatterConfig::Disable));
1437 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1438 }
1439
1440 #[test]
1441 fn test_typstyle_formatting_config() {
1442 let config = Config {
1443 formatter_mode: FormatterMode::Typstyle,
1444 ..Config::default()
1445 };
1446 let config = config.formatter();
1447 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1448
1449 let typstyle_config = match config.config {
1450 FormatterConfig::Typstyle(e) => e,
1451 _ => panic!("unexpected configuration of formatter"),
1452 };
1453
1454 assert_eq!(typstyle_config.max_width, 120);
1455 }
1456
1457 #[test]
1458 fn test_typstyle_formatting_config_set_width() {
1459 let config = Config {
1460 formatter_mode: FormatterMode::Typstyle,
1461 formatter_print_width: Some(240),
1462 ..Config::default()
1463 };
1464 let config = config.formatter();
1465 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1466
1467 let typstyle_config = match config.config {
1468 FormatterConfig::Typstyle(e) => e,
1469 _ => panic!("unexpected configuration of formatter"),
1470 };
1471
1472 assert_eq!(typstyle_config.max_width, 240);
1473 }
1474
1475 #[test]
1476 fn test_typstyle_formatting_config_set_tab_spaces() {
1477 let config = Config {
1478 formatter_mode: FormatterMode::Typstyle,
1479 formatter_indent_size: Some(8),
1480 ..Config::default()
1481 };
1482 let config = config.formatter();
1483 assert_eq!(config.position_encoding, PositionEncoding::Utf16);
1484
1485 let typstyle_config = match config.config {
1486 FormatterConfig::Typstyle(e) => e,
1487 _ => panic!("unexpected configuration of formatter"),
1488 };
1489
1490 assert_eq!(typstyle_config.tab_spaces, 8);
1491 }
1492
1493 #[test]
1494 #[cfg(feature = "preview")]
1495 fn test_default_preview_config() {
1496 let config = Config::default().preview();
1497 assert!(!config.enable_partial_rendering);
1498 assert_eq!(config.refresh_style, TaskWhen::OnType);
1499 assert_eq!(config.invert_colors, "\"never\"");
1500 }
1501
1502 #[test]
1503 #[cfg(feature = "preview")]
1504 fn test_preview_config() {
1505 let config = Config {
1506 preview: PreviewFeat {
1507 partial_rendering: true,
1508 refresh: Some(TaskWhen::OnSave),
1509 invert_colors: PreviewInvertColors::Enum(PreviewInvertColor::Auto),
1510 ..PreviewFeat::default()
1511 },
1512 ..Config::default()
1513 }
1514 .preview();
1515
1516 assert!(config.enable_partial_rendering);
1517 assert_eq!(config.refresh_style, TaskWhen::OnSave);
1518 assert_eq!(config.invert_colors, "\"auto\"");
1519 }
1520
1521 #[test]
1522 fn test_default_lsp_config_initialize() {
1523 let (_conf, err) =
1524 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1525 assert!(err.is_none());
1526 }
1527
1528 #[test]
1529 fn test_default_dap_config_initialize() {
1530 let (_conf, err) = Config::extract_dap_params(
1531 dapts::InitializeRequestArguments::default(),
1532 CompileFontArgs::default(),
1533 );
1534 assert!(err.is_none());
1535 }
1536
1537 #[test]
1538 fn test_config_package_path_from_env() {
1539 let pkg_path = Path::new(if cfg!(windows) { "C:\\pkgs" } else { "/pkgs" });
1540
1541 temp_env::with_var("TYPST_PACKAGE_CACHE_PATH", Some(pkg_path), || {
1542 let (conf, err) =
1543 Config::extract_lsp_params(InitializeParams::default(), CompileFontArgs::default());
1544 assert!(err.is_none());
1545 let applied_cache_path = conf
1546 .typst_extra_args
1547 .is_some_and(|args| args.package.package_cache_path == Some(pkg_path.into()));
1548 assert!(applied_cache_path);
1549 });
1550 }
1551
1552 #[test]
1553 #[cfg(feature = "preview")]
1554 fn test_invert_colors_validation() {
1555 fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
1556 Ok(serde_json::from_str(s)?)
1557 }
1558
1559 assert_eq!(
1560 test(r#""never""#).unwrap(),
1561 PreviewInvertColors::Enum(PreviewInvertColor::Never)
1562 );
1563 assert_eq!(
1564 test(r#""auto""#).unwrap(),
1565 PreviewInvertColors::Enum(PreviewInvertColor::Auto)
1566 );
1567 assert_eq!(
1568 test(r#""always""#).unwrap(),
1569 PreviewInvertColors::Enum(PreviewInvertColor::Always)
1570 );
1571 assert!(test(r#""e""#).is_err());
1572
1573 assert_eq!(
1574 test(r#"{"rest": "never"}"#).unwrap(),
1575 PreviewInvertColors::Object(PreviewInvertColorObject {
1576 image: PreviewInvertColor::Never,
1577 rest: PreviewInvertColor::Never,
1578 })
1579 );
1580 assert_eq!(
1581 test(r#"{"image": "always"}"#).unwrap(),
1582 PreviewInvertColors::Object(PreviewInvertColorObject {
1583 image: PreviewInvertColor::Always,
1584 rest: PreviewInvertColor::Never,
1585 })
1586 );
1587 assert_eq!(
1588 test(r#"{}"#).unwrap(),
1589 PreviewInvertColors::Object(PreviewInvertColorObject {
1590 image: PreviewInvertColor::Never,
1591 rest: PreviewInvertColor::Never,
1592 })
1593 );
1594 assert_eq!(
1595 test(r#"{"unknown": "ovo"}"#).unwrap(),
1596 PreviewInvertColors::Object(PreviewInvertColorObject {
1597 image: PreviewInvertColor::Never,
1598 rest: PreviewInvertColor::Never,
1599 })
1600 );
1601 assert!(test(r#"{"image": "e"}"#).is_err());
1602 }
1603}