1#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
13#[doc(hidden)]
14pub mod editorconfig;
15pub mod file;
16#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
17mod legacy;
18pub use file::default_config_template;
20#[cfg(feature = "cli")]
21pub use file::{
22 default_config_template_for, generate_json_schema, render_effective_config, DumpConfigFormat,
23};
24#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
25pub use legacy::convert_legacy_config_files;
26
27use std::collections::HashMap;
28
29use regex::Regex;
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
35#[serde(rename_all = "lowercase")]
36pub enum CaseStyle {
37 Lower,
39 #[default]
41 Upper,
42 Unchanged,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
49#[serde(rename_all = "lowercase")]
50pub enum LineEnding {
51 #[default]
53 Unix,
54 Windows,
56 Auto,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
63#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
64#[serde(rename_all = "kebab-case")]
65pub enum FractionalTabPolicy {
66 #[default]
68 UseSpace,
69 RoundUp,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
90#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
91#[serde(rename_all = "lowercase")]
92pub enum DangleAlign {
93 #[default]
95 Prefix,
96 Open,
98 Close,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(default)]
135pub struct Config {
136 pub disable: bool,
139
140 pub line_ending: LineEnding,
143
144 pub line_width: usize,
147 pub tab_size: usize,
150 pub use_tabchars: bool,
152 pub fractional_tab_policy: FractionalTabPolicy,
155 pub max_empty_lines: usize,
157 pub max_lines_hwrap: usize,
160 pub max_pargs_hwrap: usize,
163 pub max_subgroups_hwrap: usize,
165 pub max_rows_cmdline: usize,
168 pub always_wrap: Vec<String>,
171 pub require_valid_layout: bool,
174 pub wrap_after_first_arg: bool,
179 pub enable_sort: bool,
182 pub autosort: bool,
187
188 pub dangle_parens: bool,
191 pub dangle_align: DangleAlign,
193 pub min_prefix_chars: usize,
196 pub max_prefix_chars: usize,
199 pub separate_ctrl_name_with_space: bool,
201 pub separate_fn_name_with_space: bool,
203
204 pub command_case: CaseStyle,
207 pub keyword_case: CaseStyle,
209
210 pub enable_markup: bool,
214 pub first_comment_is_literal: bool,
216 pub literal_comment_pattern: String,
218 pub bullet_char: String,
220 pub enum_char: String,
222 pub fence_pattern: String,
224 pub ruler_pattern: String,
226 pub hashruler_min_length: usize,
228 pub canonicalize_hashrulers: bool,
230 pub explicit_trailing_pattern: String,
234
235 pub per_command_overrides: HashMap<String, PerCommandConfig>,
238
239 #[serde(default)]
244 pub experimental: Experimental,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
253#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
254#[serde(default)]
255#[non_exhaustive]
256pub struct Experimental {
257 }
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
264#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
265#[serde(deny_unknown_fields)]
266pub struct PerCommandConfig {
267 pub command_case: Option<CaseStyle>,
269 pub keyword_case: Option<CaseStyle>,
271 pub line_width: Option<usize>,
273 pub tab_size: Option<usize>,
275 pub dangle_parens: Option<bool>,
277 pub dangle_align: Option<DangleAlign>,
279 #[serde(rename = "max_hanging_wrap_positional_args")]
282 pub max_pargs_hwrap: Option<usize>,
283 #[serde(rename = "max_hanging_wrap_groups")]
285 pub max_subgroups_hwrap: Option<usize>,
286 pub wrap_after_first_arg: Option<bool>,
288}
289
290impl Default for Config {
291 fn default() -> Self {
292 Self {
293 disable: false,
294 line_ending: LineEnding::Unix,
295 line_width: 80,
296 tab_size: 2,
297 use_tabchars: false,
298 fractional_tab_policy: FractionalTabPolicy::UseSpace,
299 max_empty_lines: 1,
300 max_lines_hwrap: 2,
301 max_pargs_hwrap: 6,
302 max_subgroups_hwrap: 2,
303 max_rows_cmdline: 2,
304 always_wrap: Vec::new(),
305 require_valid_layout: false,
306 wrap_after_first_arg: false,
307 enable_sort: false,
308 autosort: false,
309 dangle_parens: false,
310 dangle_align: DangleAlign::Prefix,
311 min_prefix_chars: 4,
312 max_prefix_chars: 10,
313 separate_ctrl_name_with_space: false,
314 separate_fn_name_with_space: false,
315 command_case: CaseStyle::Lower,
316 keyword_case: CaseStyle::Upper,
317 enable_markup: true,
318 first_comment_is_literal: true,
319 literal_comment_pattern: String::new(),
320 bullet_char: "*".to_string(),
321 enum_char: ".".to_string(),
322 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
323 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
324 hashruler_min_length: 10,
325 canonicalize_hashrulers: true,
326 explicit_trailing_pattern: "#<".to_string(),
327 per_command_overrides: HashMap::new(),
328 experimental: Experimental::default(),
329 }
330 }
331}
332
333const CONTROL_FLOW_COMMANDS: &[&str] = &[
335 "if",
336 "elseif",
337 "else",
338 "endif",
339 "foreach",
340 "endforeach",
341 "while",
342 "endwhile",
343 "break",
344 "continue",
345 "return",
346 "block",
347 "endblock",
348];
349
350const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
353
354impl Config {
355 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
358 let lower = command_name.to_ascii_lowercase();
359 let per_cmd = self.per_command_overrides.get(&lower);
360
361 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
362 self.separate_ctrl_name_with_space
363 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
364 self.separate_fn_name_with_space
365 } else {
366 false
367 };
368
369 CommandConfig {
370 global: self,
371 per_cmd,
372 space_before_paren,
373 }
374 }
375
376 pub fn apply_command_case(&self, name: &str) -> String {
378 apply_case(self.command_case, name)
379 }
380
381 pub fn apply_keyword_case(&self, keyword: &str) -> String {
383 apply_case(self.keyword_case, keyword)
384 }
385
386 pub fn indent_str(&self) -> String {
388 if self.use_tabchars {
389 "\t".to_string()
390 } else {
391 " ".repeat(self.tab_size)
392 }
393 }
394
395 pub fn validate_patterns(&self) -> Result<(), String> {
400 let patterns = [
401 ("literal_comment_pattern", &self.literal_comment_pattern),
402 ("explicit_trailing_pattern", &self.explicit_trailing_pattern),
403 ("fence_pattern", &self.fence_pattern),
404 ("ruler_pattern", &self.ruler_pattern),
405 ];
406 for (name, pattern) in &patterns {
407 if !pattern.is_empty() {
408 if let Err(err) = Regex::new(pattern) {
409 return Err(format!("invalid regex in {name}: {err}"));
410 }
411 }
412 }
413 Ok(())
414 }
415
416 pub(crate) fn compiled_patterns(&self) -> Result<CompiledPatterns, String> {
421 Ok(CompiledPatterns {
422 literal_comment: compile_optional(
423 "literal_comment_pattern",
424 &self.literal_comment_pattern,
425 )?,
426 explicit_trailing: compile_optional(
427 "explicit_trailing_pattern",
428 &self.explicit_trailing_pattern,
429 )?,
430 })
431 }
432}
433
434fn compile_optional(name: &str, pattern: &str) -> Result<Option<Regex>, String> {
435 if pattern.is_empty() {
436 Ok(None)
437 } else {
438 Regex::new(pattern)
439 .map(Some)
440 .map_err(|err| format!("invalid regex in {name}: {err}"))
441 }
442}
443
444pub(crate) struct CompiledPatterns {
446 pub(crate) literal_comment: Option<Regex>,
448 #[allow(dead_code)]
452 pub(crate) explicit_trailing: Option<Regex>,
453}
454
455#[derive(Debug)]
458pub struct CommandConfig<'a> {
459 global: &'a Config,
461 per_cmd: Option<&'a PerCommandConfig>,
462 space_before_paren: bool,
464}
465
466impl CommandConfig<'_> {
467 pub fn space_before_paren(&self) -> bool {
469 self.space_before_paren
470 }
471
472 pub(crate) fn global(&self) -> &Config {
473 self.global
474 }
475
476 pub fn line_width(&self) -> usize {
478 self.per_cmd
479 .and_then(|p| p.line_width)
480 .unwrap_or(self.global.line_width)
481 }
482
483 pub fn tab_size(&self) -> usize {
485 self.per_cmd
486 .and_then(|p| p.tab_size)
487 .unwrap_or(self.global.tab_size)
488 }
489
490 pub fn dangle_parens(&self) -> bool {
492 self.per_cmd
493 .and_then(|p| p.dangle_parens)
494 .unwrap_or(self.global.dangle_parens)
495 }
496
497 pub fn dangle_align(&self) -> DangleAlign {
499 self.per_cmd
500 .and_then(|p| p.dangle_align)
501 .unwrap_or(self.global.dangle_align)
502 }
503
504 pub fn command_case(&self) -> CaseStyle {
506 self.per_cmd
507 .and_then(|p| p.command_case)
508 .unwrap_or(self.global.command_case)
509 }
510
511 pub fn keyword_case(&self) -> CaseStyle {
513 self.per_cmd
514 .and_then(|p| p.keyword_case)
515 .unwrap_or(self.global.keyword_case)
516 }
517
518 pub fn max_pargs_hwrap(&self) -> usize {
521 self.per_cmd
522 .and_then(|p| p.max_pargs_hwrap)
523 .unwrap_or(self.global.max_pargs_hwrap)
524 }
525
526 pub fn max_subgroups_hwrap(&self) -> usize {
528 self.per_cmd
529 .and_then(|p| p.max_subgroups_hwrap)
530 .unwrap_or(self.global.max_subgroups_hwrap)
531 }
532
533 pub fn wrap_after_first_arg(&self, spec_value: Option<bool>) -> bool {
538 self.per_cmd
539 .and_then(|p| p.wrap_after_first_arg)
540 .or(spec_value)
541 .unwrap_or(self.global.wrap_after_first_arg)
542 }
543
544 pub fn indent_str(&self) -> String {
546 if self.global.use_tabchars {
547 "\t".to_string()
548 } else {
549 " ".repeat(self.tab_size())
550 }
551 }
552}
553
554fn apply_case(style: CaseStyle, s: &str) -> String {
555 match style {
556 CaseStyle::Lower => s.to_ascii_lowercase(),
557 CaseStyle::Upper => s.to_ascii_uppercase(),
558 CaseStyle::Unchanged => s.to_string(),
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
569 fn for_command_control_flow_sets_space_before_paren() {
570 let config = Config {
571 separate_ctrl_name_with_space: true,
572 ..Config::default()
573 };
574 for cmd in ["if", "elseif", "foreach", "while", "return"] {
575 let cc = config.for_command(cmd);
576 assert!(
577 cc.space_before_paren(),
578 "{cmd} should have space_before_paren=true"
579 );
580 }
581 }
582
583 #[test]
584 fn for_command_fn_definition_sets_space_before_paren() {
585 let config = Config {
586 separate_fn_name_with_space: true,
587 ..Config::default()
588 };
589 for cmd in ["function", "endfunction", "macro", "endmacro"] {
590 let cc = config.for_command(cmd);
591 assert!(
592 cc.space_before_paren(),
593 "{cmd} should have space_before_paren=true"
594 );
595 }
596 }
597
598 #[test]
599 fn for_command_regular_command_no_space_before_paren() {
600 let config = Config {
601 separate_ctrl_name_with_space: true,
602 separate_fn_name_with_space: true,
603 ..Config::default()
604 };
605 let cc = config.for_command("message");
606 assert!(
607 !cc.space_before_paren(),
608 "message should not have space_before_paren"
609 );
610 }
611
612 #[test]
613 fn for_command_lookup_is_case_insensitive() {
614 let mut overrides = HashMap::new();
615 overrides.insert(
616 "message".to_string(),
617 PerCommandConfig {
618 line_width: Some(120),
619 ..Default::default()
620 },
621 );
622 let config = Config {
623 per_command_overrides: overrides,
624 ..Config::default()
625 };
626 assert_eq!(config.for_command("MESSAGE").line_width(), 120);
628 }
629
630 #[test]
633 fn command_config_returns_global_defaults_when_no_override() {
634 let config = Config::default();
635 let cc = config.for_command("set");
636 assert_eq!(cc.line_width(), config.line_width);
637 assert_eq!(cc.tab_size(), config.tab_size);
638 assert_eq!(cc.dangle_parens(), config.dangle_parens);
639 assert_eq!(cc.command_case(), config.command_case);
640 assert_eq!(cc.keyword_case(), config.keyword_case);
641 assert_eq!(cc.max_pargs_hwrap(), config.max_pargs_hwrap);
642 assert_eq!(cc.max_subgroups_hwrap(), config.max_subgroups_hwrap);
643 }
644
645 #[test]
646 fn command_config_per_command_overrides_take_effect() {
647 let mut overrides = HashMap::new();
648 overrides.insert(
649 "set".to_string(),
650 PerCommandConfig {
651 line_width: Some(120),
652 tab_size: Some(4),
653 dangle_parens: Some(true),
654 dangle_align: Some(DangleAlign::Open),
655 command_case: Some(CaseStyle::Upper),
656 keyword_case: Some(CaseStyle::Lower),
657 max_pargs_hwrap: Some(10),
658 max_subgroups_hwrap: Some(5),
659 wrap_after_first_arg: None,
660 },
661 );
662 let config = Config {
663 per_command_overrides: overrides,
664 ..Config::default()
665 };
666 let cc = config.for_command("set");
667 assert_eq!(cc.line_width(), 120);
668 assert_eq!(cc.tab_size(), 4);
669 assert!(cc.dangle_parens());
670 assert_eq!(cc.dangle_align(), DangleAlign::Open);
671 assert_eq!(cc.command_case(), CaseStyle::Upper);
672 assert_eq!(cc.keyword_case(), CaseStyle::Lower);
673 assert_eq!(cc.max_pargs_hwrap(), 10);
674 assert_eq!(cc.max_subgroups_hwrap(), 5);
675 }
676
677 #[test]
678 fn indent_str_spaces() {
679 let config = Config {
680 tab_size: 4,
681 use_tabchars: false,
682 ..Config::default()
683 };
684 assert_eq!(config.indent_str(), " ");
685 assert_eq!(config.for_command("set").indent_str(), " ");
686 }
687
688 #[test]
689 fn indent_str_tab() {
690 let config = Config {
691 use_tabchars: true,
692 ..Config::default()
693 };
694 assert_eq!(config.indent_str(), "\t");
695 assert_eq!(config.for_command("set").indent_str(), "\t");
696 }
697
698 #[test]
701 fn apply_command_case_lower() {
702 let config = Config {
703 command_case: CaseStyle::Lower,
704 ..Config::default()
705 };
706 assert_eq!(
707 config.apply_command_case("TARGET_LINK_LIBRARIES"),
708 "target_link_libraries"
709 );
710 }
711
712 #[test]
713 fn apply_command_case_upper() {
714 let config = Config {
715 command_case: CaseStyle::Upper,
716 ..Config::default()
717 };
718 assert_eq!(
719 config.apply_command_case("target_link_libraries"),
720 "TARGET_LINK_LIBRARIES"
721 );
722 }
723
724 #[test]
725 fn apply_command_case_unchanged() {
726 let config = Config {
727 command_case: CaseStyle::Unchanged,
728 ..Config::default()
729 };
730 assert_eq!(
731 config.apply_command_case("Target_Link_Libraries"),
732 "Target_Link_Libraries"
733 );
734 }
735
736 #[test]
737 fn apply_keyword_case_variants() {
738 let config_upper = Config {
739 keyword_case: CaseStyle::Upper,
740 ..Config::default()
741 };
742 assert_eq!(config_upper.apply_keyword_case("public"), "PUBLIC");
743
744 let config_lower = Config {
745 keyword_case: CaseStyle::Lower,
746 ..Config::default()
747 };
748 assert_eq!(config_lower.apply_keyword_case("PUBLIC"), "public");
749 }
750
751 #[test]
754 fn error_layout_too_wide_display() {
755 use crate::error::Error;
756 let err = Error::LayoutTooWide {
757 line_no: 5,
758 width: 95,
759 limit: 80,
760 };
761 let msg = err.to_string();
762 assert!(msg.contains("5"), "should mention line number");
763 assert!(msg.contains("95"), "should mention actual width");
764 assert!(msg.contains("80"), "should mention limit");
765 }
766
767 #[test]
768 fn error_formatter_display() {
769 use crate::error::Error;
770 let err = Error::Formatter("something went wrong".to_string());
771 assert!(err.to_string().contains("something went wrong"));
772 }
773}