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)]
135#[serde(default)]
136pub struct Config {
137 pub disable: bool,
140
141 pub line_ending: LineEnding,
144
145 pub line_width: usize,
148 pub tab_size: usize,
151 pub use_tabchars: bool,
153 pub fractional_tab_policy: FractionalTabPolicy,
156 pub max_empty_lines: usize,
158 pub max_lines_hwrap: usize,
161 pub max_pargs_hwrap: usize,
164 pub max_subgroups_hwrap: usize,
166 pub max_rows_cmdline: usize,
169 pub always_wrap: Vec<String>,
172 pub require_valid_layout: bool,
175
176 pub dangle_parens: bool,
179 pub dangle_align: DangleAlign,
181 pub min_prefix_chars: usize,
184 pub max_prefix_chars: usize,
187 pub separate_ctrl_name_with_space: bool,
189 pub separate_fn_name_with_space: bool,
191
192 pub command_case: CaseStyle,
195 pub keyword_case: CaseStyle,
197
198 pub enable_markup: bool,
201 pub reflow_comments: bool,
203 pub first_comment_is_literal: bool,
205 pub literal_comment_pattern: String,
207 pub bullet_char: String,
209 pub enum_char: String,
211 pub fence_pattern: String,
213 pub ruler_pattern: String,
215 pub hashruler_min_length: usize,
217 pub canonicalize_hashrulers: bool,
219 pub explicit_trailing_pattern: String,
223
224 pub per_command_overrides: HashMap<String, PerCommandConfig>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
232#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
233#[serde(deny_unknown_fields)]
234pub struct PerCommandConfig {
235 pub command_case: Option<CaseStyle>,
237 pub keyword_case: Option<CaseStyle>,
239 pub line_width: Option<usize>,
241 pub tab_size: Option<usize>,
243 pub dangle_parens: Option<bool>,
245 pub dangle_align: Option<DangleAlign>,
247 #[serde(rename = "max_hanging_wrap_positional_args")]
250 pub max_pargs_hwrap: Option<usize>,
251 #[serde(rename = "max_hanging_wrap_groups")]
253 pub max_subgroups_hwrap: Option<usize>,
254}
255
256impl Default for Config {
257 fn default() -> Self {
258 Self {
259 disable: false,
260 line_ending: LineEnding::Unix,
261 line_width: 80,
262 tab_size: 2,
263 use_tabchars: false,
264 fractional_tab_policy: FractionalTabPolicy::UseSpace,
265 max_empty_lines: 1,
266 max_lines_hwrap: 2,
267 max_pargs_hwrap: 6,
268 max_subgroups_hwrap: 2,
269 max_rows_cmdline: 2,
270 always_wrap: Vec::new(),
271 require_valid_layout: false,
272 dangle_parens: false,
273 dangle_align: DangleAlign::Prefix,
274 min_prefix_chars: 4,
275 max_prefix_chars: 10,
276 separate_ctrl_name_with_space: false,
277 separate_fn_name_with_space: false,
278 command_case: CaseStyle::Lower,
279 keyword_case: CaseStyle::Upper,
280 enable_markup: true,
281 reflow_comments: false,
282 first_comment_is_literal: true,
283 literal_comment_pattern: String::new(),
284 bullet_char: "*".to_string(),
285 enum_char: ".".to_string(),
286 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
287 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
288 hashruler_min_length: 10,
289 canonicalize_hashrulers: true,
290 explicit_trailing_pattern: "#<".to_string(),
291 per_command_overrides: HashMap::new(),
292 }
293 }
294}
295
296const CONTROL_FLOW_COMMANDS: &[&str] = &[
298 "if",
299 "elseif",
300 "else",
301 "endif",
302 "foreach",
303 "endforeach",
304 "while",
305 "endwhile",
306 "break",
307 "continue",
308 "return",
309 "block",
310 "endblock",
311];
312
313const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
316
317impl Config {
318 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
321 let lower = command_name.to_ascii_lowercase();
322 let per_cmd = self.per_command_overrides.get(&lower);
323
324 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
325 self.separate_ctrl_name_with_space
326 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
327 self.separate_fn_name_with_space
328 } else {
329 false
330 };
331
332 CommandConfig {
333 global: self,
334 per_cmd,
335 space_before_paren,
336 }
337 }
338
339 pub fn apply_command_case(&self, name: &str) -> String {
341 apply_case(self.command_case, name)
342 }
343
344 pub fn apply_keyword_case(&self, keyword: &str) -> String {
346 apply_case(self.keyword_case, keyword)
347 }
348
349 pub fn indent_str(&self) -> String {
351 if self.use_tabchars {
352 "\t".to_string()
353 } else {
354 " ".repeat(self.tab_size)
355 }
356 }
357
358 pub fn validate_patterns(&self) -> Result<(), String> {
363 let patterns = [
364 ("literal_comment_pattern", &self.literal_comment_pattern),
365 ("explicit_trailing_pattern", &self.explicit_trailing_pattern),
366 ("fence_pattern", &self.fence_pattern),
367 ("ruler_pattern", &self.ruler_pattern),
368 ];
369 for (name, pattern) in &patterns {
370 if !pattern.is_empty() {
371 if let Err(err) = Regex::new(pattern) {
372 return Err(format!("invalid regex in {name}: {err}"));
373 }
374 }
375 }
376 Ok(())
377 }
378
379 pub(crate) fn compiled_patterns(&self) -> Result<CompiledPatterns, String> {
384 Ok(CompiledPatterns {
385 literal_comment: compile_optional(
386 "literal_comment_pattern",
387 &self.literal_comment_pattern,
388 )?,
389 explicit_trailing: compile_optional(
390 "explicit_trailing_pattern",
391 &self.explicit_trailing_pattern,
392 )?,
393 })
394 }
395}
396
397fn compile_optional(name: &str, pattern: &str) -> Result<Option<Regex>, String> {
398 if pattern.is_empty() {
399 Ok(None)
400 } else {
401 Regex::new(pattern)
402 .map(Some)
403 .map_err(|err| format!("invalid regex in {name}: {err}"))
404 }
405}
406
407pub(crate) struct CompiledPatterns {
409 pub(crate) literal_comment: Option<Regex>,
411 pub(crate) explicit_trailing: Option<Regex>,
413}
414
415#[derive(Debug)]
418pub struct CommandConfig<'a> {
419 global: &'a Config,
421 per_cmd: Option<&'a PerCommandConfig>,
422 space_before_paren: bool,
424}
425
426impl CommandConfig<'_> {
427 pub fn space_before_paren(&self) -> bool {
429 self.space_before_paren
430 }
431
432 pub(crate) fn global(&self) -> &Config {
433 self.global
434 }
435
436 pub fn line_width(&self) -> usize {
438 self.per_cmd
439 .and_then(|p| p.line_width)
440 .unwrap_or(self.global.line_width)
441 }
442
443 pub fn tab_size(&self) -> usize {
445 self.per_cmd
446 .and_then(|p| p.tab_size)
447 .unwrap_or(self.global.tab_size)
448 }
449
450 pub fn dangle_parens(&self) -> bool {
452 self.per_cmd
453 .and_then(|p| p.dangle_parens)
454 .unwrap_or(self.global.dangle_parens)
455 }
456
457 pub fn dangle_align(&self) -> DangleAlign {
459 self.per_cmd
460 .and_then(|p| p.dangle_align)
461 .unwrap_or(self.global.dangle_align)
462 }
463
464 pub fn command_case(&self) -> CaseStyle {
466 self.per_cmd
467 .and_then(|p| p.command_case)
468 .unwrap_or(self.global.command_case)
469 }
470
471 pub fn keyword_case(&self) -> CaseStyle {
473 self.per_cmd
474 .and_then(|p| p.keyword_case)
475 .unwrap_or(self.global.keyword_case)
476 }
477
478 pub fn max_pargs_hwrap(&self) -> usize {
481 self.per_cmd
482 .and_then(|p| p.max_pargs_hwrap)
483 .unwrap_or(self.global.max_pargs_hwrap)
484 }
485
486 pub fn max_subgroups_hwrap(&self) -> usize {
488 self.per_cmd
489 .and_then(|p| p.max_subgroups_hwrap)
490 .unwrap_or(self.global.max_subgroups_hwrap)
491 }
492
493 pub fn indent_str(&self) -> String {
495 if self.global.use_tabchars {
496 "\t".to_string()
497 } else {
498 " ".repeat(self.tab_size())
499 }
500 }
501}
502
503fn apply_case(style: CaseStyle, s: &str) -> String {
504 match style {
505 CaseStyle::Lower => s.to_ascii_lowercase(),
506 CaseStyle::Upper => s.to_ascii_uppercase(),
507 CaseStyle::Unchanged => s.to_string(),
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
518 fn for_command_control_flow_sets_space_before_paren() {
519 let config = Config {
520 separate_ctrl_name_with_space: true,
521 ..Config::default()
522 };
523 for cmd in ["if", "elseif", "foreach", "while", "return"] {
524 let cc = config.for_command(cmd);
525 assert!(
526 cc.space_before_paren(),
527 "{cmd} should have space_before_paren=true"
528 );
529 }
530 }
531
532 #[test]
533 fn for_command_fn_definition_sets_space_before_paren() {
534 let config = Config {
535 separate_fn_name_with_space: true,
536 ..Config::default()
537 };
538 for cmd in ["function", "endfunction", "macro", "endmacro"] {
539 let cc = config.for_command(cmd);
540 assert!(
541 cc.space_before_paren(),
542 "{cmd} should have space_before_paren=true"
543 );
544 }
545 }
546
547 #[test]
548 fn for_command_regular_command_no_space_before_paren() {
549 let config = Config {
550 separate_ctrl_name_with_space: true,
551 separate_fn_name_with_space: true,
552 ..Config::default()
553 };
554 let cc = config.for_command("message");
555 assert!(
556 !cc.space_before_paren(),
557 "message should not have space_before_paren"
558 );
559 }
560
561 #[test]
562 fn for_command_lookup_is_case_insensitive() {
563 let mut overrides = HashMap::new();
564 overrides.insert(
565 "message".to_string(),
566 PerCommandConfig {
567 line_width: Some(120),
568 ..Default::default()
569 },
570 );
571 let config = Config {
572 per_command_overrides: overrides,
573 ..Config::default()
574 };
575 assert_eq!(config.for_command("MESSAGE").line_width(), 120);
577 }
578
579 #[test]
582 fn command_config_returns_global_defaults_when_no_override() {
583 let config = Config::default();
584 let cc = config.for_command("set");
585 assert_eq!(cc.line_width(), config.line_width);
586 assert_eq!(cc.tab_size(), config.tab_size);
587 assert_eq!(cc.dangle_parens(), config.dangle_parens);
588 assert_eq!(cc.command_case(), config.command_case);
589 assert_eq!(cc.keyword_case(), config.keyword_case);
590 assert_eq!(cc.max_pargs_hwrap(), config.max_pargs_hwrap);
591 assert_eq!(cc.max_subgroups_hwrap(), config.max_subgroups_hwrap);
592 }
593
594 #[test]
595 fn command_config_per_command_overrides_take_effect() {
596 let mut overrides = HashMap::new();
597 overrides.insert(
598 "set".to_string(),
599 PerCommandConfig {
600 line_width: Some(120),
601 tab_size: Some(4),
602 dangle_parens: Some(true),
603 dangle_align: Some(DangleAlign::Open),
604 command_case: Some(CaseStyle::Upper),
605 keyword_case: Some(CaseStyle::Lower),
606 max_pargs_hwrap: Some(10),
607 max_subgroups_hwrap: Some(5),
608 },
609 );
610 let config = Config {
611 per_command_overrides: overrides,
612 ..Config::default()
613 };
614 let cc = config.for_command("set");
615 assert_eq!(cc.line_width(), 120);
616 assert_eq!(cc.tab_size(), 4);
617 assert!(cc.dangle_parens());
618 assert_eq!(cc.dangle_align(), DangleAlign::Open);
619 assert_eq!(cc.command_case(), CaseStyle::Upper);
620 assert_eq!(cc.keyword_case(), CaseStyle::Lower);
621 assert_eq!(cc.max_pargs_hwrap(), 10);
622 assert_eq!(cc.max_subgroups_hwrap(), 5);
623 }
624
625 #[test]
626 fn indent_str_spaces() {
627 let config = Config {
628 tab_size: 4,
629 use_tabchars: false,
630 ..Config::default()
631 };
632 assert_eq!(config.indent_str(), " ");
633 assert_eq!(config.for_command("set").indent_str(), " ");
634 }
635
636 #[test]
637 fn indent_str_tab() {
638 let config = Config {
639 use_tabchars: true,
640 ..Config::default()
641 };
642 assert_eq!(config.indent_str(), "\t");
643 assert_eq!(config.for_command("set").indent_str(), "\t");
644 }
645
646 #[test]
649 fn apply_command_case_lower() {
650 let config = Config {
651 command_case: CaseStyle::Lower,
652 ..Config::default()
653 };
654 assert_eq!(
655 config.apply_command_case("TARGET_LINK_LIBRARIES"),
656 "target_link_libraries"
657 );
658 }
659
660 #[test]
661 fn apply_command_case_upper() {
662 let config = Config {
663 command_case: CaseStyle::Upper,
664 ..Config::default()
665 };
666 assert_eq!(
667 config.apply_command_case("target_link_libraries"),
668 "TARGET_LINK_LIBRARIES"
669 );
670 }
671
672 #[test]
673 fn apply_command_case_unchanged() {
674 let config = Config {
675 command_case: CaseStyle::Unchanged,
676 ..Config::default()
677 };
678 assert_eq!(
679 config.apply_command_case("Target_Link_Libraries"),
680 "Target_Link_Libraries"
681 );
682 }
683
684 #[test]
685 fn apply_keyword_case_variants() {
686 let config_upper = Config {
687 keyword_case: CaseStyle::Upper,
688 ..Config::default()
689 };
690 assert_eq!(config_upper.apply_keyword_case("public"), "PUBLIC");
691
692 let config_lower = Config {
693 keyword_case: CaseStyle::Lower,
694 ..Config::default()
695 };
696 assert_eq!(config_lower.apply_keyword_case("PUBLIC"), "public");
697 }
698
699 #[test]
702 fn error_layout_too_wide_display() {
703 use crate::error::Error;
704 let err = Error::LayoutTooWide {
705 line_no: 5,
706 width: 95,
707 limit: 80,
708 };
709 let msg = err.to_string();
710 assert!(msg.contains("5"), "should mention line number");
711 assert!(msg.contains("95"), "should mention actual width");
712 assert!(msg.contains("80"), "should mention limit");
713 }
714
715 #[test]
716 fn error_formatter_display() {
717 use crate::error::Error;
718 let err = Error::Formatter("something went wrong".to_string());
719 assert!(err.to_string().contains("something went wrong"));
720 }
721}