1#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
13pub mod file;
14#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
15mod legacy;
16#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
18pub use file::{
19 default_config_template, default_config_template_for, generate_json_schema,
20 render_effective_config, DumpConfigFormat,
21};
22#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
23pub use legacy::convert_legacy_config_files;
24
25use std::collections::HashMap;
26
27use regex::Regex;
28use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
32#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
33#[serde(rename_all = "lowercase")]
34pub enum CaseStyle {
35 Lower,
37 #[default]
39 Upper,
40 Unchanged,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
46#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
47#[serde(rename_all = "lowercase")]
48pub enum LineEnding {
49 #[default]
51 Unix,
52 Windows,
54 Auto,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
61#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
62#[serde(rename_all = "kebab-case")]
63pub enum FractionalTabPolicy {
64 #[default]
66 UseSpace,
67 RoundUp,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
89#[serde(rename_all = "lowercase")]
90pub enum DangleAlign {
91 #[default]
93 Prefix,
94 Open,
96 Close,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(default)]
134pub struct Config {
135 pub disable: bool,
138
139 pub line_ending: LineEnding,
142
143 pub line_width: usize,
146 pub tab_size: usize,
149 pub use_tabchars: bool,
151 pub fractional_tab_policy: FractionalTabPolicy,
154 pub max_empty_lines: usize,
156 pub max_lines_hwrap: usize,
159 pub max_pargs_hwrap: usize,
162 pub max_subgroups_hwrap: usize,
164 pub max_rows_cmdline: usize,
167 pub always_wrap: Vec<String>,
170 pub require_valid_layout: bool,
173
174 pub dangle_parens: bool,
177 pub dangle_align: DangleAlign,
179 pub min_prefix_chars: usize,
182 pub max_prefix_chars: usize,
185 pub separate_ctrl_name_with_space: bool,
187 pub separate_fn_name_with_space: bool,
189
190 pub command_case: CaseStyle,
193 pub keyword_case: CaseStyle,
195
196 pub enable_markup: bool,
199 pub reflow_comments: bool,
201 pub first_comment_is_literal: bool,
203 pub literal_comment_pattern: String,
205 pub bullet_char: String,
207 pub enum_char: String,
209 pub fence_pattern: String,
211 pub ruler_pattern: String,
213 pub hashruler_min_length: usize,
215 pub canonicalize_hashrulers: bool,
217 pub explicit_trailing_pattern: String,
221
222 pub per_command_overrides: HashMap<String, PerCommandConfig>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
230#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
231#[serde(deny_unknown_fields)]
232pub struct PerCommandConfig {
233 pub command_case: Option<CaseStyle>,
235 pub keyword_case: Option<CaseStyle>,
237 pub line_width: Option<usize>,
239 pub tab_size: Option<usize>,
241 pub dangle_parens: Option<bool>,
243 pub dangle_align: Option<DangleAlign>,
245 #[serde(rename = "max_hanging_wrap_positional_args")]
248 pub max_pargs_hwrap: Option<usize>,
249 #[serde(rename = "max_hanging_wrap_groups")]
251 pub max_subgroups_hwrap: Option<usize>,
252}
253
254impl Default for Config {
255 fn default() -> Self {
256 Self {
257 disable: false,
258 line_ending: LineEnding::Unix,
259 line_width: 80,
260 tab_size: 2,
261 use_tabchars: false,
262 fractional_tab_policy: FractionalTabPolicy::UseSpace,
263 max_empty_lines: 1,
264 max_lines_hwrap: 2,
265 max_pargs_hwrap: 6,
266 max_subgroups_hwrap: 2,
267 max_rows_cmdline: 2,
268 always_wrap: Vec::new(),
269 require_valid_layout: false,
270 dangle_parens: false,
271 dangle_align: DangleAlign::Prefix,
272 min_prefix_chars: 4,
273 max_prefix_chars: 10,
274 separate_ctrl_name_with_space: false,
275 separate_fn_name_with_space: false,
276 command_case: CaseStyle::Lower,
277 keyword_case: CaseStyle::Upper,
278 enable_markup: true,
279 reflow_comments: false,
280 first_comment_is_literal: true,
281 literal_comment_pattern: String::new(),
282 bullet_char: "*".to_string(),
283 enum_char: ".".to_string(),
284 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
285 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
286 hashruler_min_length: 10,
287 canonicalize_hashrulers: true,
288 explicit_trailing_pattern: "#<".to_string(),
289 per_command_overrides: HashMap::new(),
290 }
291 }
292}
293
294const CONTROL_FLOW_COMMANDS: &[&str] = &[
296 "if",
297 "elseif",
298 "else",
299 "endif",
300 "foreach",
301 "endforeach",
302 "while",
303 "endwhile",
304 "break",
305 "continue",
306 "return",
307 "block",
308 "endblock",
309];
310
311const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
314
315impl Config {
316 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
319 let lower = command_name.to_ascii_lowercase();
320 let per_cmd = self.per_command_overrides.get(&lower);
321
322 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
323 self.separate_ctrl_name_with_space
324 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
325 self.separate_fn_name_with_space
326 } else {
327 false
328 };
329
330 CommandConfig {
331 global: self,
332 per_cmd,
333 space_before_paren,
334 }
335 }
336
337 pub fn apply_command_case(&self, name: &str) -> String {
339 apply_case(self.command_case, name)
340 }
341
342 pub fn apply_keyword_case(&self, keyword: &str) -> String {
344 apply_case(self.keyword_case, keyword)
345 }
346
347 pub fn indent_str(&self) -> String {
349 if self.use_tabchars {
350 "\t".to_string()
351 } else {
352 " ".repeat(self.tab_size)
353 }
354 }
355
356 pub fn validate_patterns(&self) -> Result<(), String> {
361 let patterns = [
362 ("literal_comment_pattern", &self.literal_comment_pattern),
363 ("explicit_trailing_pattern", &self.explicit_trailing_pattern),
364 ("fence_pattern", &self.fence_pattern),
365 ("ruler_pattern", &self.ruler_pattern),
366 ];
367 for (name, pattern) in &patterns {
368 if !pattern.is_empty() {
369 if let Err(err) = Regex::new(pattern) {
370 return Err(format!("invalid regex in {name}: {err}"));
371 }
372 }
373 }
374 Ok(())
375 }
376
377 pub fn compiled_patterns(&self) -> CompiledPatterns {
382 CompiledPatterns {
383 literal_comment: compile_optional(&self.literal_comment_pattern),
384 explicit_trailing: compile_optional(&self.explicit_trailing_pattern),
385 fence: compile_optional(&self.fence_pattern),
386 ruler: compile_optional(&self.ruler_pattern),
387 }
388 }
389}
390
391fn compile_optional(pattern: &str) -> Option<Regex> {
392 if pattern.is_empty() {
393 None
394 } else {
395 Regex::new(pattern).ok()
396 }
397}
398
399pub struct CompiledPatterns {
403 pub literal_comment: Option<Regex>,
405 pub explicit_trailing: Option<Regex>,
407 pub fence: Option<Regex>,
409 pub ruler: Option<Regex>,
411}
412
413#[derive(Debug)]
416pub struct CommandConfig<'a> {
417 pub global: &'a Config,
419 per_cmd: Option<&'a PerCommandConfig>,
420 pub space_before_paren: bool,
422}
423
424impl CommandConfig<'_> {
425 pub fn line_width(&self) -> usize {
427 self.per_cmd
428 .and_then(|p| p.line_width)
429 .unwrap_or(self.global.line_width)
430 }
431
432 pub fn tab_size(&self) -> usize {
434 self.per_cmd
435 .and_then(|p| p.tab_size)
436 .unwrap_or(self.global.tab_size)
437 }
438
439 pub fn dangle_parens(&self) -> bool {
441 self.per_cmd
442 .and_then(|p| p.dangle_parens)
443 .unwrap_or(self.global.dangle_parens)
444 }
445
446 pub fn dangle_align(&self) -> DangleAlign {
448 self.per_cmd
449 .and_then(|p| p.dangle_align)
450 .unwrap_or(self.global.dangle_align)
451 }
452
453 pub fn command_case(&self) -> CaseStyle {
455 self.per_cmd
456 .and_then(|p| p.command_case)
457 .unwrap_or(self.global.command_case)
458 }
459
460 pub fn keyword_case(&self) -> CaseStyle {
462 self.per_cmd
463 .and_then(|p| p.keyword_case)
464 .unwrap_or(self.global.keyword_case)
465 }
466
467 pub fn max_pargs_hwrap(&self) -> usize {
470 self.per_cmd
471 .and_then(|p| p.max_pargs_hwrap)
472 .unwrap_or(self.global.max_pargs_hwrap)
473 }
474
475 pub fn max_subgroups_hwrap(&self) -> usize {
477 self.per_cmd
478 .and_then(|p| p.max_subgroups_hwrap)
479 .unwrap_or(self.global.max_subgroups_hwrap)
480 }
481
482 pub fn indent_str(&self) -> String {
484 if self.global.use_tabchars {
485 "\t".to_string()
486 } else {
487 " ".repeat(self.tab_size())
488 }
489 }
490}
491
492fn apply_case(style: CaseStyle, s: &str) -> String {
493 match style {
494 CaseStyle::Lower => s.to_ascii_lowercase(),
495 CaseStyle::Upper => s.to_ascii_uppercase(),
496 CaseStyle::Unchanged => s.to_string(),
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
507 fn for_command_control_flow_sets_space_before_paren() {
508 let config = Config {
509 separate_ctrl_name_with_space: true,
510 ..Config::default()
511 };
512 for cmd in ["if", "elseif", "foreach", "while", "return"] {
513 let cc = config.for_command(cmd);
514 assert!(
515 cc.space_before_paren,
516 "{cmd} should have space_before_paren=true"
517 );
518 }
519 }
520
521 #[test]
522 fn for_command_fn_definition_sets_space_before_paren() {
523 let config = Config {
524 separate_fn_name_with_space: true,
525 ..Config::default()
526 };
527 for cmd in ["function", "endfunction", "macro", "endmacro"] {
528 let cc = config.for_command(cmd);
529 assert!(
530 cc.space_before_paren,
531 "{cmd} should have space_before_paren=true"
532 );
533 }
534 }
535
536 #[test]
537 fn for_command_regular_command_no_space_before_paren() {
538 let config = Config {
539 separate_ctrl_name_with_space: true,
540 separate_fn_name_with_space: true,
541 ..Config::default()
542 };
543 let cc = config.for_command("message");
544 assert!(
545 !cc.space_before_paren,
546 "message should not have space_before_paren"
547 );
548 }
549
550 #[test]
551 fn for_command_lookup_is_case_insensitive() {
552 let mut overrides = HashMap::new();
553 overrides.insert(
554 "message".to_string(),
555 PerCommandConfig {
556 line_width: Some(120),
557 ..Default::default()
558 },
559 );
560 let config = Config {
561 per_command_overrides: overrides,
562 ..Config::default()
563 };
564 assert_eq!(config.for_command("MESSAGE").line_width(), 120);
566 }
567
568 #[test]
571 fn command_config_returns_global_defaults_when_no_override() {
572 let config = Config::default();
573 let cc = config.for_command("set");
574 assert_eq!(cc.line_width(), config.line_width);
575 assert_eq!(cc.tab_size(), config.tab_size);
576 assert_eq!(cc.dangle_parens(), config.dangle_parens);
577 assert_eq!(cc.command_case(), config.command_case);
578 assert_eq!(cc.keyword_case(), config.keyword_case);
579 assert_eq!(cc.max_pargs_hwrap(), config.max_pargs_hwrap);
580 assert_eq!(cc.max_subgroups_hwrap(), config.max_subgroups_hwrap);
581 }
582
583 #[test]
584 fn command_config_per_command_overrides_take_effect() {
585 let mut overrides = HashMap::new();
586 overrides.insert(
587 "set".to_string(),
588 PerCommandConfig {
589 line_width: Some(120),
590 tab_size: Some(4),
591 dangle_parens: Some(true),
592 dangle_align: Some(DangleAlign::Open),
593 command_case: Some(CaseStyle::Upper),
594 keyword_case: Some(CaseStyle::Lower),
595 max_pargs_hwrap: Some(10),
596 max_subgroups_hwrap: Some(5),
597 },
598 );
599 let config = Config {
600 per_command_overrides: overrides,
601 ..Config::default()
602 };
603 let cc = config.for_command("set");
604 assert_eq!(cc.line_width(), 120);
605 assert_eq!(cc.tab_size(), 4);
606 assert!(cc.dangle_parens());
607 assert_eq!(cc.dangle_align(), DangleAlign::Open);
608 assert_eq!(cc.command_case(), CaseStyle::Upper);
609 assert_eq!(cc.keyword_case(), CaseStyle::Lower);
610 assert_eq!(cc.max_pargs_hwrap(), 10);
611 assert_eq!(cc.max_subgroups_hwrap(), 5);
612 }
613
614 #[test]
615 fn indent_str_spaces() {
616 let config = Config {
617 tab_size: 4,
618 use_tabchars: false,
619 ..Config::default()
620 };
621 assert_eq!(config.indent_str(), " ");
622 assert_eq!(config.for_command("set").indent_str(), " ");
623 }
624
625 #[test]
626 fn indent_str_tab() {
627 let config = Config {
628 use_tabchars: true,
629 ..Config::default()
630 };
631 assert_eq!(config.indent_str(), "\t");
632 assert_eq!(config.for_command("set").indent_str(), "\t");
633 }
634
635 #[test]
638 fn apply_command_case_lower() {
639 let config = Config {
640 command_case: CaseStyle::Lower,
641 ..Config::default()
642 };
643 assert_eq!(
644 config.apply_command_case("TARGET_LINK_LIBRARIES"),
645 "target_link_libraries"
646 );
647 }
648
649 #[test]
650 fn apply_command_case_upper() {
651 let config = Config {
652 command_case: CaseStyle::Upper,
653 ..Config::default()
654 };
655 assert_eq!(
656 config.apply_command_case("target_link_libraries"),
657 "TARGET_LINK_LIBRARIES"
658 );
659 }
660
661 #[test]
662 fn apply_command_case_unchanged() {
663 let config = Config {
664 command_case: CaseStyle::Unchanged,
665 ..Config::default()
666 };
667 assert_eq!(
668 config.apply_command_case("Target_Link_Libraries"),
669 "Target_Link_Libraries"
670 );
671 }
672
673 #[test]
674 fn apply_keyword_case_variants() {
675 let config_upper = Config {
676 keyword_case: CaseStyle::Upper,
677 ..Config::default()
678 };
679 assert_eq!(config_upper.apply_keyword_case("public"), "PUBLIC");
680
681 let config_lower = Config {
682 keyword_case: CaseStyle::Lower,
683 ..Config::default()
684 };
685 assert_eq!(config_lower.apply_keyword_case("PUBLIC"), "public");
686 }
687
688 #[test]
691 fn error_layout_too_wide_display() {
692 use crate::error::Error;
693 let err = Error::LayoutTooWide {
694 line_no: 5,
695 width: 95,
696 limit: 80,
697 };
698 let msg = err.to_string();
699 assert!(msg.contains("5"), "should mention line number");
700 assert!(msg.contains("95"), "should mention actual width");
701 assert!(msg.contains("80"), "should mention limit");
702 }
703
704 #[test]
705 fn error_formatter_display() {
706 use crate::error::Error;
707 let err = Error::Formatter("something went wrong".to_string());
708 assert!(err.to_string().contains("something went wrong"));
709 }
710}