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