#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
#[doc(hidden)]
pub mod editorconfig;
pub mod file;
#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
mod legacy;
pub use file::default_config_template;
#[cfg(feature = "cli")]
pub use file::{
default_config_template_for, generate_json_schema, render_effective_config, DumpConfigFormat,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
pub use legacy::convert_legacy_config_files;
use std::collections::HashMap;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum CaseStyle {
Lower,
#[default]
Upper,
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum LineEnding {
#[default]
Unix,
Windows,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum, schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum FractionalTabPolicy {
#[default]
UseSpace,
RoundUp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum DangleAlign {
#[default]
Prefix,
Open,
Close,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub disable: bool,
pub line_ending: LineEnding,
pub line_width: usize,
pub tab_size: usize,
pub use_tabchars: bool,
pub fractional_tab_policy: FractionalTabPolicy,
pub max_empty_lines: usize,
pub max_lines_hwrap: usize,
pub max_pargs_hwrap: usize,
pub max_subgroups_hwrap: usize,
pub max_rows_cmdline: usize,
pub always_wrap: Vec<String>,
pub require_valid_layout: bool,
pub wrap_after_first_arg: bool,
pub enable_sort: bool,
pub autosort: bool,
pub dangle_parens: bool,
pub dangle_align: DangleAlign,
pub min_prefix_chars: usize,
pub max_prefix_chars: usize,
pub separate_ctrl_name_with_space: bool,
pub separate_fn_name_with_space: bool,
pub command_case: CaseStyle,
pub keyword_case: CaseStyle,
pub enable_markup: bool,
pub first_comment_is_literal: bool,
pub literal_comment_pattern: String,
pub bullet_char: String,
pub enum_char: String,
pub fence_pattern: String,
pub ruler_pattern: String,
pub hashruler_min_length: usize,
pub canonicalize_hashrulers: bool,
pub explicit_trailing_pattern: String,
pub per_command_overrides: HashMap<String, PerCommandConfig>,
#[serde(default)]
pub experimental: Experimental,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
#[serde(default)]
#[non_exhaustive]
pub struct Experimental {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "cli", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PerCommandConfig {
pub command_case: Option<CaseStyle>,
pub keyword_case: Option<CaseStyle>,
pub line_width: Option<usize>,
pub tab_size: Option<usize>,
pub dangle_parens: Option<bool>,
pub dangle_align: Option<DangleAlign>,
#[serde(rename = "max_hanging_wrap_positional_args")]
pub max_pargs_hwrap: Option<usize>,
#[serde(rename = "max_hanging_wrap_groups")]
pub max_subgroups_hwrap: Option<usize>,
pub wrap_after_first_arg: Option<bool>,
}
impl Default for Config {
fn default() -> Self {
Self {
disable: false,
line_ending: LineEnding::Unix,
line_width: 80,
tab_size: 2,
use_tabchars: false,
fractional_tab_policy: FractionalTabPolicy::UseSpace,
max_empty_lines: 1,
max_lines_hwrap: 2,
max_pargs_hwrap: 6,
max_subgroups_hwrap: 2,
max_rows_cmdline: 2,
always_wrap: Vec::new(),
require_valid_layout: false,
wrap_after_first_arg: false,
enable_sort: false,
autosort: false,
dangle_parens: false,
dangle_align: DangleAlign::Prefix,
min_prefix_chars: 4,
max_prefix_chars: 10,
separate_ctrl_name_with_space: false,
separate_fn_name_with_space: false,
command_case: CaseStyle::Lower,
keyword_case: CaseStyle::Upper,
enable_markup: true,
first_comment_is_literal: true,
literal_comment_pattern: String::new(),
bullet_char: "*".to_string(),
enum_char: ".".to_string(),
fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
hashruler_min_length: 10,
canonicalize_hashrulers: true,
explicit_trailing_pattern: "#<".to_string(),
per_command_overrides: HashMap::new(),
experimental: Experimental::default(),
}
}
}
const CONTROL_FLOW_COMMANDS: &[&str] = &[
"if",
"elseif",
"else",
"endif",
"foreach",
"endforeach",
"while",
"endwhile",
"break",
"continue",
"return",
"block",
"endblock",
];
const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
impl Config {
pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
let lower = command_name.to_ascii_lowercase();
let per_cmd = self.per_command_overrides.get(&lower);
let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
self.separate_ctrl_name_with_space
} else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
self.separate_fn_name_with_space
} else {
false
};
CommandConfig {
global: self,
per_cmd,
space_before_paren,
}
}
pub fn apply_command_case(&self, name: &str) -> String {
apply_case(self.command_case, name)
}
pub fn apply_keyword_case(&self, keyword: &str) -> String {
apply_case(self.keyword_case, keyword)
}
pub fn indent_str(&self) -> String {
if self.use_tabchars {
"\t".to_string()
} else {
" ".repeat(self.tab_size)
}
}
pub fn validate_patterns(&self) -> Result<(), String> {
let patterns = [
("literal_comment_pattern", &self.literal_comment_pattern),
("explicit_trailing_pattern", &self.explicit_trailing_pattern),
("fence_pattern", &self.fence_pattern),
("ruler_pattern", &self.ruler_pattern),
];
for (name, pattern) in &patterns {
if !pattern.is_empty() {
if let Err(err) = Regex::new(pattern) {
return Err(format!("invalid regex in {name}: {err}"));
}
}
}
Ok(())
}
pub(crate) fn compiled_patterns(&self) -> Result<CompiledPatterns, String> {
Ok(CompiledPatterns {
literal_comment: compile_optional(
"literal_comment_pattern",
&self.literal_comment_pattern,
)?,
explicit_trailing: compile_optional(
"explicit_trailing_pattern",
&self.explicit_trailing_pattern,
)?,
})
}
}
fn compile_optional(name: &str, pattern: &str) -> Result<Option<Regex>, String> {
if pattern.is_empty() {
Ok(None)
} else {
Regex::new(pattern)
.map(Some)
.map_err(|err| format!("invalid regex in {name}: {err}"))
}
}
pub(crate) struct CompiledPatterns {
pub(crate) literal_comment: Option<Regex>,
#[allow(dead_code)]
pub(crate) explicit_trailing: Option<Regex>,
}
#[derive(Debug)]
pub struct CommandConfig<'a> {
global: &'a Config,
per_cmd: Option<&'a PerCommandConfig>,
space_before_paren: bool,
}
impl CommandConfig<'_> {
pub fn space_before_paren(&self) -> bool {
self.space_before_paren
}
pub(crate) fn global(&self) -> &Config {
self.global
}
pub fn line_width(&self) -> usize {
self.per_cmd
.and_then(|p| p.line_width)
.unwrap_or(self.global.line_width)
}
pub fn tab_size(&self) -> usize {
self.per_cmd
.and_then(|p| p.tab_size)
.unwrap_or(self.global.tab_size)
}
pub fn dangle_parens(&self) -> bool {
self.per_cmd
.and_then(|p| p.dangle_parens)
.unwrap_or(self.global.dangle_parens)
}
pub fn dangle_align(&self) -> DangleAlign {
self.per_cmd
.and_then(|p| p.dangle_align)
.unwrap_or(self.global.dangle_align)
}
pub fn command_case(&self) -> CaseStyle {
self.per_cmd
.and_then(|p| p.command_case)
.unwrap_or(self.global.command_case)
}
pub fn keyword_case(&self) -> CaseStyle {
self.per_cmd
.and_then(|p| p.keyword_case)
.unwrap_or(self.global.keyword_case)
}
pub fn max_pargs_hwrap(&self) -> usize {
self.per_cmd
.and_then(|p| p.max_pargs_hwrap)
.unwrap_or(self.global.max_pargs_hwrap)
}
pub fn max_subgroups_hwrap(&self) -> usize {
self.per_cmd
.and_then(|p| p.max_subgroups_hwrap)
.unwrap_or(self.global.max_subgroups_hwrap)
}
pub fn wrap_after_first_arg(&self, spec_value: Option<bool>) -> bool {
self.per_cmd
.and_then(|p| p.wrap_after_first_arg)
.or(spec_value)
.unwrap_or(self.global.wrap_after_first_arg)
}
pub fn indent_str(&self) -> String {
if self.global.use_tabchars {
"\t".to_string()
} else {
" ".repeat(self.tab_size())
}
}
}
fn apply_case(style: CaseStyle, s: &str) -> String {
match style {
CaseStyle::Lower => s.to_ascii_lowercase(),
CaseStyle::Upper => s.to_ascii_uppercase(),
CaseStyle::Unchanged => s.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_command_control_flow_sets_space_before_paren() {
let config = Config {
separate_ctrl_name_with_space: true,
..Config::default()
};
for cmd in ["if", "elseif", "foreach", "while", "return"] {
let cc = config.for_command(cmd);
assert!(
cc.space_before_paren(),
"{cmd} should have space_before_paren=true"
);
}
}
#[test]
fn for_command_fn_definition_sets_space_before_paren() {
let config = Config {
separate_fn_name_with_space: true,
..Config::default()
};
for cmd in ["function", "endfunction", "macro", "endmacro"] {
let cc = config.for_command(cmd);
assert!(
cc.space_before_paren(),
"{cmd} should have space_before_paren=true"
);
}
}
#[test]
fn for_command_regular_command_no_space_before_paren() {
let config = Config {
separate_ctrl_name_with_space: true,
separate_fn_name_with_space: true,
..Config::default()
};
let cc = config.for_command("message");
assert!(
!cc.space_before_paren(),
"message should not have space_before_paren"
);
}
#[test]
fn for_command_lookup_is_case_insensitive() {
let mut overrides = HashMap::new();
overrides.insert(
"message".to_string(),
PerCommandConfig {
line_width: Some(120),
..Default::default()
},
);
let config = Config {
per_command_overrides: overrides,
..Config::default()
};
assert_eq!(config.for_command("MESSAGE").line_width(), 120);
}
#[test]
fn command_config_returns_global_defaults_when_no_override() {
let config = Config::default();
let cc = config.for_command("set");
assert_eq!(cc.line_width(), config.line_width);
assert_eq!(cc.tab_size(), config.tab_size);
assert_eq!(cc.dangle_parens(), config.dangle_parens);
assert_eq!(cc.command_case(), config.command_case);
assert_eq!(cc.keyword_case(), config.keyword_case);
assert_eq!(cc.max_pargs_hwrap(), config.max_pargs_hwrap);
assert_eq!(cc.max_subgroups_hwrap(), config.max_subgroups_hwrap);
}
#[test]
fn command_config_per_command_overrides_take_effect() {
let mut overrides = HashMap::new();
overrides.insert(
"set".to_string(),
PerCommandConfig {
line_width: Some(120),
tab_size: Some(4),
dangle_parens: Some(true),
dangle_align: Some(DangleAlign::Open),
command_case: Some(CaseStyle::Upper),
keyword_case: Some(CaseStyle::Lower),
max_pargs_hwrap: Some(10),
max_subgroups_hwrap: Some(5),
wrap_after_first_arg: None,
},
);
let config = Config {
per_command_overrides: overrides,
..Config::default()
};
let cc = config.for_command("set");
assert_eq!(cc.line_width(), 120);
assert_eq!(cc.tab_size(), 4);
assert!(cc.dangle_parens());
assert_eq!(cc.dangle_align(), DangleAlign::Open);
assert_eq!(cc.command_case(), CaseStyle::Upper);
assert_eq!(cc.keyword_case(), CaseStyle::Lower);
assert_eq!(cc.max_pargs_hwrap(), 10);
assert_eq!(cc.max_subgroups_hwrap(), 5);
}
#[test]
fn indent_str_spaces() {
let config = Config {
tab_size: 4,
use_tabchars: false,
..Config::default()
};
assert_eq!(config.indent_str(), " ");
assert_eq!(config.for_command("set").indent_str(), " ");
}
#[test]
fn indent_str_tab() {
let config = Config {
use_tabchars: true,
..Config::default()
};
assert_eq!(config.indent_str(), "\t");
assert_eq!(config.for_command("set").indent_str(), "\t");
}
#[test]
fn apply_command_case_lower() {
let config = Config {
command_case: CaseStyle::Lower,
..Config::default()
};
assert_eq!(
config.apply_command_case("TARGET_LINK_LIBRARIES"),
"target_link_libraries"
);
}
#[test]
fn apply_command_case_upper() {
let config = Config {
command_case: CaseStyle::Upper,
..Config::default()
};
assert_eq!(
config.apply_command_case("target_link_libraries"),
"TARGET_LINK_LIBRARIES"
);
}
#[test]
fn apply_command_case_unchanged() {
let config = Config {
command_case: CaseStyle::Unchanged,
..Config::default()
};
assert_eq!(
config.apply_command_case("Target_Link_Libraries"),
"Target_Link_Libraries"
);
}
#[test]
fn apply_keyword_case_variants() {
let config_upper = Config {
keyword_case: CaseStyle::Upper,
..Config::default()
};
assert_eq!(config_upper.apply_keyword_case("public"), "PUBLIC");
let config_lower = Config {
keyword_case: CaseStyle::Lower,
..Config::default()
};
assert_eq!(config_lower.apply_keyword_case("PUBLIC"), "public");
}
#[test]
fn error_layout_too_wide_display() {
use crate::error::Error;
let err = Error::LayoutTooWide {
line_no: 5,
width: 95,
limit: 80,
};
let msg = err.to_string();
assert!(msg.contains("5"), "should mention line number");
assert!(msg.contains("95"), "should mention actual width");
assert!(msg.contains("80"), "should mention limit");
}
#[test]
fn error_formatter_display() {
use crate::error::Error;
let err = Error::Formatter("something went wrong".to_string());
assert!(err.to_string().contains("something went wrong"));
}
}