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 serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
32#[serde(rename_all = "lowercase")]
33pub enum CaseStyle {
34 Lower,
36 #[default]
38 Upper,
39 Unchanged,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
45#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
46#[serde(rename_all = "lowercase")]
47pub enum LineEnding {
48 #[default]
50 Unix,
51 Windows,
53 Auto,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
60#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
61#[serde(rename_all = "kebab-case")]
62pub enum FractionalTabPolicy {
63 #[default]
65 UseSpace,
66 RoundUp,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
87#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
88#[serde(rename_all = "lowercase")]
89pub enum DangleAlign {
90 #[default]
92 Prefix,
93 Open,
95 Close,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(default)]
133pub struct Config {
134 pub disable: bool,
137
138 pub line_ending: LineEnding,
141
142 pub line_width: usize,
145 pub tab_size: usize,
148 pub use_tabchars: bool,
150 pub fractional_tab_policy: FractionalTabPolicy,
153 pub max_empty_lines: usize,
155 pub max_lines_hwrap: usize,
158 pub max_pargs_hwrap: usize,
161 pub max_subgroups_hwrap: usize,
163 pub max_rows_cmdline: usize,
166 pub always_wrap: Vec<String>,
169 pub require_valid_layout: bool,
172
173 pub dangle_parens: bool,
176 pub dangle_align: DangleAlign,
178 pub min_prefix_chars: usize,
181 pub max_prefix_chars: usize,
184 pub separate_ctrl_name_with_space: bool,
186 pub separate_fn_name_with_space: bool,
188
189 pub command_case: CaseStyle,
192 pub keyword_case: CaseStyle,
194
195 pub enable_markup: bool,
198 pub reflow_comments: bool,
200 pub first_comment_is_literal: bool,
202 pub literal_comment_pattern: String,
204 pub bullet_char: String,
206 pub enum_char: String,
208 pub fence_pattern: String,
210 pub ruler_pattern: String,
212 pub hashruler_min_length: usize,
214 pub canonicalize_hashrulers: bool,
216 pub explicit_trailing_pattern: String,
220
221 pub per_command_overrides: HashMap<String, PerCommandConfig>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
229#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
230#[serde(deny_unknown_fields)]
231pub struct PerCommandConfig {
232 pub command_case: Option<CaseStyle>,
234 pub keyword_case: Option<CaseStyle>,
236 pub line_width: Option<usize>,
238 pub tab_size: Option<usize>,
240 pub dangle_parens: Option<bool>,
242 pub dangle_align: Option<DangleAlign>,
244 #[serde(rename = "max_hanging_wrap_positional_args")]
247 pub max_pargs_hwrap: Option<usize>,
248 #[serde(rename = "max_hanging_wrap_groups")]
250 pub max_subgroups_hwrap: Option<usize>,
251}
252
253impl Default for Config {
254 fn default() -> Self {
255 Self {
256 disable: false,
257 line_ending: LineEnding::Unix,
258 line_width: 80,
259 tab_size: 2,
260 use_tabchars: false,
261 fractional_tab_policy: FractionalTabPolicy::UseSpace,
262 max_empty_lines: 1,
263 max_lines_hwrap: 2,
264 max_pargs_hwrap: 6,
265 max_subgroups_hwrap: 2,
266 max_rows_cmdline: 2,
267 always_wrap: Vec::new(),
268 require_valid_layout: false,
269 dangle_parens: false,
270 dangle_align: DangleAlign::Prefix,
271 min_prefix_chars: 4,
272 max_prefix_chars: 10,
273 separate_ctrl_name_with_space: false,
274 separate_fn_name_with_space: false,
275 command_case: CaseStyle::Lower,
276 keyword_case: CaseStyle::Upper,
277 enable_markup: true,
278 reflow_comments: false,
279 first_comment_is_literal: true,
280 literal_comment_pattern: String::new(),
281 bullet_char: "*".to_string(),
282 enum_char: ".".to_string(),
283 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
284 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
285 hashruler_min_length: 10,
286 canonicalize_hashrulers: true,
287 explicit_trailing_pattern: "#<".to_string(),
288 per_command_overrides: HashMap::new(),
289 }
290 }
291}
292
293const CONTROL_FLOW_COMMANDS: &[&str] = &[
295 "if",
296 "elseif",
297 "else",
298 "endif",
299 "foreach",
300 "endforeach",
301 "while",
302 "endwhile",
303 "break",
304 "continue",
305 "return",
306 "block",
307 "endblock",
308];
309
310const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
313
314impl Config {
315 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
318 let lower = command_name.to_ascii_lowercase();
319 let per_cmd = self.per_command_overrides.get(&lower);
320
321 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
322 self.separate_ctrl_name_with_space
323 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
324 self.separate_fn_name_with_space
325 } else {
326 false
327 };
328
329 CommandConfig {
330 global: self,
331 per_cmd,
332 space_before_paren,
333 }
334 }
335
336 pub fn apply_command_case(&self, name: &str) -> String {
338 apply_case(self.command_case, name)
339 }
340
341 pub fn apply_keyword_case(&self, keyword: &str) -> String {
343 apply_case(self.keyword_case, keyword)
344 }
345
346 pub fn indent_str(&self) -> String {
348 if self.use_tabchars {
349 "\t".to_string()
350 } else {
351 " ".repeat(self.tab_size)
352 }
353 }
354}
355
356#[derive(Debug)]
359pub struct CommandConfig<'a> {
360 pub global: &'a Config,
362 per_cmd: Option<&'a PerCommandConfig>,
363 pub space_before_paren: bool,
365}
366
367impl CommandConfig<'_> {
368 pub fn line_width(&self) -> usize {
370 self.per_cmd
371 .and_then(|p| p.line_width)
372 .unwrap_or(self.global.line_width)
373 }
374
375 pub fn tab_size(&self) -> usize {
377 self.per_cmd
378 .and_then(|p| p.tab_size)
379 .unwrap_or(self.global.tab_size)
380 }
381
382 pub fn dangle_parens(&self) -> bool {
384 self.per_cmd
385 .and_then(|p| p.dangle_parens)
386 .unwrap_or(self.global.dangle_parens)
387 }
388
389 pub fn dangle_align(&self) -> DangleAlign {
391 self.per_cmd
392 .and_then(|p| p.dangle_align)
393 .unwrap_or(self.global.dangle_align)
394 }
395
396 pub fn command_case(&self) -> CaseStyle {
398 self.per_cmd
399 .and_then(|p| p.command_case)
400 .unwrap_or(self.global.command_case)
401 }
402
403 pub fn keyword_case(&self) -> CaseStyle {
405 self.per_cmd
406 .and_then(|p| p.keyword_case)
407 .unwrap_or(self.global.keyword_case)
408 }
409
410 pub fn max_pargs_hwrap(&self) -> usize {
413 self.per_cmd
414 .and_then(|p| p.max_pargs_hwrap)
415 .unwrap_or(self.global.max_pargs_hwrap)
416 }
417
418 pub fn max_subgroups_hwrap(&self) -> usize {
420 self.per_cmd
421 .and_then(|p| p.max_subgroups_hwrap)
422 .unwrap_or(self.global.max_subgroups_hwrap)
423 }
424
425 pub fn indent_str(&self) -> String {
427 if self.global.use_tabchars {
428 "\t".to_string()
429 } else {
430 " ".repeat(self.tab_size())
431 }
432 }
433}
434
435fn apply_case(style: CaseStyle, s: &str) -> String {
436 match style {
437 CaseStyle::Lower => s.to_ascii_lowercase(),
438 CaseStyle::Upper => s.to_ascii_uppercase(),
439 CaseStyle::Unchanged => s.to_string(),
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
450 fn for_command_control_flow_sets_space_before_paren() {
451 let config = Config {
452 separate_ctrl_name_with_space: true,
453 ..Config::default()
454 };
455 for cmd in ["if", "elseif", "foreach", "while", "return"] {
456 let cc = config.for_command(cmd);
457 assert!(
458 cc.space_before_paren,
459 "{cmd} should have space_before_paren=true"
460 );
461 }
462 }
463
464 #[test]
465 fn for_command_fn_definition_sets_space_before_paren() {
466 let config = Config {
467 separate_fn_name_with_space: true,
468 ..Config::default()
469 };
470 for cmd in ["function", "endfunction", "macro", "endmacro"] {
471 let cc = config.for_command(cmd);
472 assert!(
473 cc.space_before_paren,
474 "{cmd} should have space_before_paren=true"
475 );
476 }
477 }
478
479 #[test]
480 fn for_command_regular_command_no_space_before_paren() {
481 let config = Config {
482 separate_ctrl_name_with_space: true,
483 separate_fn_name_with_space: true,
484 ..Config::default()
485 };
486 let cc = config.for_command("message");
487 assert!(
488 !cc.space_before_paren,
489 "message should not have space_before_paren"
490 );
491 }
492
493 #[test]
494 fn for_command_lookup_is_case_insensitive() {
495 let mut overrides = HashMap::new();
496 overrides.insert(
497 "message".to_string(),
498 PerCommandConfig {
499 line_width: Some(120),
500 ..Default::default()
501 },
502 );
503 let config = Config {
504 per_command_overrides: overrides,
505 ..Config::default()
506 };
507 assert_eq!(config.for_command("MESSAGE").line_width(), 120);
509 }
510
511 #[test]
514 fn command_config_returns_global_defaults_when_no_override() {
515 let config = Config::default();
516 let cc = config.for_command("set");
517 assert_eq!(cc.line_width(), config.line_width);
518 assert_eq!(cc.tab_size(), config.tab_size);
519 assert_eq!(cc.dangle_parens(), config.dangle_parens);
520 assert_eq!(cc.command_case(), config.command_case);
521 assert_eq!(cc.keyword_case(), config.keyword_case);
522 assert_eq!(cc.max_pargs_hwrap(), config.max_pargs_hwrap);
523 assert_eq!(cc.max_subgroups_hwrap(), config.max_subgroups_hwrap);
524 }
525
526 #[test]
527 fn command_config_per_command_overrides_take_effect() {
528 let mut overrides = HashMap::new();
529 overrides.insert(
530 "set".to_string(),
531 PerCommandConfig {
532 line_width: Some(120),
533 tab_size: Some(4),
534 dangle_parens: Some(true),
535 dangle_align: Some(DangleAlign::Open),
536 command_case: Some(CaseStyle::Upper),
537 keyword_case: Some(CaseStyle::Lower),
538 max_pargs_hwrap: Some(10),
539 max_subgroups_hwrap: Some(5),
540 },
541 );
542 let config = Config {
543 per_command_overrides: overrides,
544 ..Config::default()
545 };
546 let cc = config.for_command("set");
547 assert_eq!(cc.line_width(), 120);
548 assert_eq!(cc.tab_size(), 4);
549 assert!(cc.dangle_parens());
550 assert_eq!(cc.dangle_align(), DangleAlign::Open);
551 assert_eq!(cc.command_case(), CaseStyle::Upper);
552 assert_eq!(cc.keyword_case(), CaseStyle::Lower);
553 assert_eq!(cc.max_pargs_hwrap(), 10);
554 assert_eq!(cc.max_subgroups_hwrap(), 5);
555 }
556
557 #[test]
558 fn indent_str_spaces() {
559 let config = Config {
560 tab_size: 4,
561 use_tabchars: false,
562 ..Config::default()
563 };
564 assert_eq!(config.indent_str(), " ");
565 assert_eq!(config.for_command("set").indent_str(), " ");
566 }
567
568 #[test]
569 fn indent_str_tab() {
570 let config = Config {
571 use_tabchars: true,
572 ..Config::default()
573 };
574 assert_eq!(config.indent_str(), "\t");
575 assert_eq!(config.for_command("set").indent_str(), "\t");
576 }
577
578 #[test]
581 fn apply_command_case_lower() {
582 let config = Config {
583 command_case: CaseStyle::Lower,
584 ..Config::default()
585 };
586 assert_eq!(
587 config.apply_command_case("TARGET_LINK_LIBRARIES"),
588 "target_link_libraries"
589 );
590 }
591
592 #[test]
593 fn apply_command_case_upper() {
594 let config = Config {
595 command_case: CaseStyle::Upper,
596 ..Config::default()
597 };
598 assert_eq!(
599 config.apply_command_case("target_link_libraries"),
600 "TARGET_LINK_LIBRARIES"
601 );
602 }
603
604 #[test]
605 fn apply_command_case_unchanged() {
606 let config = Config {
607 command_case: CaseStyle::Unchanged,
608 ..Config::default()
609 };
610 assert_eq!(
611 config.apply_command_case("Target_Link_Libraries"),
612 "Target_Link_Libraries"
613 );
614 }
615
616 #[test]
617 fn apply_keyword_case_variants() {
618 let config_upper = Config {
619 keyword_case: CaseStyle::Upper,
620 ..Config::default()
621 };
622 assert_eq!(config_upper.apply_keyword_case("public"), "PUBLIC");
623
624 let config_lower = Config {
625 keyword_case: CaseStyle::Lower,
626 ..Config::default()
627 };
628 assert_eq!(config_lower.apply_keyword_case("PUBLIC"), "public");
629 }
630
631 #[test]
634 fn error_layout_too_wide_display() {
635 use crate::error::Error;
636 let err = Error::LayoutTooWide {
637 line_no: 5,
638 width: 95,
639 limit: 80,
640 };
641 let msg = err.to_string();
642 assert!(msg.contains("5"), "should mention line number");
643 assert!(msg.contains("95"), "should mention actual width");
644 assert!(msg.contains("80"), "should mention limit");
645 }
646
647 #[test]
648 fn error_formatter_display() {
649 use crate::error::Error;
650 let err = Error::Formatter("something went wrong".to_string());
651 assert!(err.to_string().contains("something went wrong"));
652 }
653}