use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize};
use panache_parser::{Extensions, Flavor, PandocCompat, ParserOptions};
use super::formatter_presets;
#[derive(Debug, Clone, PartialEq)]
pub struct FormatterConfig {
pub cmd: String,
pub args: Vec<String>,
pub enabled: bool,
pub stdin: bool,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum FormatterValue {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct FormatterDefinition {
pub preset: Option<String>,
pub cmd: Option<String>,
pub args: Option<Vec<String>>,
#[serde(alias = "prepend_args")]
pub prepend_args: Option<Vec<String>>,
#[serde(alias = "append_args")]
pub append_args: Option<Vec<String>>,
pub stdin: Option<bool>,
pub enabled: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(default)]
struct RawFormatterConfig {
preset: Option<String>,
cmd: Option<String>,
args: Option<Vec<String>>,
enabled: bool,
stdin: bool,
}
impl Default for RawFormatterConfig {
fn default() -> Self {
Self {
preset: None,
cmd: None,
args: None,
enabled: true,
stdin: true,
}
}
}
impl<'de> Deserialize<'de> for FormatterConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = RawFormatterConfig::deserialize(deserializer)?;
if raw.preset.is_some() && raw.cmd.is_some() {
return Err(serde::de::Error::custom(
"FormatterConfig: 'preset' and 'cmd' are mutually exclusive - use one or the other",
));
}
if let Some(preset_name) = raw.preset {
let preset = get_formatter_preset(&preset_name).ok_or_else(|| {
let available = formatter_preset_names().join(", ");
serde::de::Error::custom(format!(
"Unknown formatter preset: '{}'. Available presets: {}",
preset_name, available
))
})?;
Ok(FormatterConfig {
cmd: preset.cmd,
args: preset.args,
enabled: raw.enabled,
stdin: preset.stdin,
})
} else if let Some(cmd) = raw.cmd {
Ok(FormatterConfig {
cmd,
args: raw.args.unwrap_or_default(),
enabled: raw.enabled,
stdin: raw.stdin,
})
} else {
Ok(FormatterConfig {
cmd: String::new(),
args: raw.args.unwrap_or_default(),
enabled: raw.enabled,
stdin: raw.stdin,
})
}
}
}
impl Default for FormatterConfig {
fn default() -> Self {
Self {
cmd: String::new(),
args: Vec::new(),
enabled: true,
stdin: true,
}
}
}
fn get_formatter_preset(name: &str) -> Option<FormatterConfig> {
formatter_presets::get_formatter_preset(name)
}
fn formatter_preset_names() -> &'static [&'static str] {
formatter_presets::formatter_preset_names()
}
fn formatter_preset_supported_languages(name: &str) -> Option<&'static [&'static str]> {
formatter_presets::formatter_preset_supported_languages(name)
}
fn normalize_formatter_language(language: &str) -> String {
language.trim().to_ascii_lowercase().replace('_', "-")
}
fn validate_formatter_language_for_preset(lang: &str, formatter_name: &str) -> Result<(), String> {
let Some(supported) = formatter_preset_supported_languages(formatter_name) else {
return Ok(()); };
let normalized_lang = normalize_formatter_language(lang);
let matches = supported
.iter()
.any(|supported_lang| *supported_lang == normalized_lang);
if matches {
return Ok(());
}
Err(format!(
"Language '{}': formatter '{}' does not support this language. Supported languages: {}",
lang,
formatter_name,
supported.join(", ")
))
}
#[allow(dead_code)]
pub fn default_formatters() -> HashMap<String, FormatterConfig> {
let mut map = HashMap::new();
map.insert("r".to_string(), get_formatter_preset("air").unwrap());
map.insert("python".to_string(), get_formatter_preset("ruff").unwrap());
map
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum MathDelimiterStyle {
#[default]
Preserve,
Dollars,
Backslash,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum TabStopMode {
#[default]
Normalize,
Preserve,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct StyleConfig {
pub wrap: Option<WrapMode>,
pub blank_lines: BlankLines,
pub math_delimiter_style: MathDelimiterStyle,
pub math_indent: usize,
pub tab_stops: TabStopMode,
pub tab_width: usize,
pub built_in_greedy_wrap: bool,
}
impl Default for StyleConfig {
fn default() -> Self {
Self {
wrap: Some(WrapMode::Reflow),
blank_lines: BlankLines::Collapse,
math_delimiter_style: MathDelimiterStyle::default(),
math_indent: 0,
tab_stops: TabStopMode::Normalize,
tab_width: 4,
built_in_greedy_wrap: true,
}
}
}
impl StyleConfig {
}
#[derive(Debug, Clone, Serialize, PartialEq, Default)]
pub struct LintConfig {
pub rules: HashMap<String, bool>,
}
impl LintConfig {
fn normalize_rule_name(name: &str) -> String {
name.trim().to_lowercase().replace('_', "-")
}
fn normalize(mut self) -> Self {
self.rules = self
.rules
.into_iter()
.map(|(name, enabled)| (Self::normalize_rule_name(&name), enabled))
.collect();
self
}
pub fn is_rule_enabled(&self, rule_name: &str) -> bool {
let normalized = Self::normalize_rule_name(rule_name);
self.rules.get(&normalized).copied().unwrap_or(true)
}
}
impl<'de> Deserialize<'de> for LintConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = toml::Value::deserialize(deserializer)?;
let mut rules = HashMap::new();
let mut used_legacy_shape = false;
let mut table = value
.as_table()
.cloned()
.ok_or_else(|| serde::de::Error::custom("expected [lint] table"))?;
if let Some(rules_value) = table.remove("rules") {
let rules_table = rules_value
.as_table()
.ok_or_else(|| serde::de::Error::custom("[lint.rules] must be a table"))?;
for (name, enabled) in rules_table {
let enabled = enabled.as_bool().ok_or_else(|| {
serde::de::Error::custom(format!(
"[lint.rules] entry '{}' must be true or false",
name
))
})?;
rules.insert(name.clone(), enabled);
}
}
for (name, enabled) in table {
let enabled = enabled.as_bool().ok_or_else(|| {
serde::de::Error::custom(format!(
"Unsupported [lint] key '{}'; use [lint.rules] for rule toggles",
name
))
})?;
used_legacy_shape = true;
rules.insert(name, enabled);
}
if used_legacy_shape {
eprintln!(
"Warning: [lint] rule = true/false is deprecated; use [lint.rules] rule = true/false."
);
}
Ok(Self { rules }.normalize())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct RawConfig {
#[serde(default)]
flavor: Flavor,
#[serde(default)]
extensions: Option<toml::Value>,
#[serde(default)]
line_ending: Option<LineEnding>,
#[serde(default = "default_line_width")]
line_width: usize,
#[serde(default)]
pandoc_compat: Option<PandocCompat>,
#[serde(default)]
#[serde(rename = "format")]
format_section: Option<StyleConfig>,
#[serde(default)]
style: Option<StyleConfig>,
#[serde(default)]
math_indent: usize,
#[serde(default)]
math_delimiter_style: MathDelimiterStyle,
#[serde(default)]
wrap: Option<WrapMode>,
#[serde(default = "default_blank_lines")]
blank_lines: BlankLines,
#[serde(default)]
tab_stops: TabStopMode,
#[serde(default = "default_tab_width")]
tab_width: usize,
#[serde(default)]
formatters: Option<toml::Value>,
#[serde(default)]
external_max_parallel: Option<usize>,
#[serde(default)]
linters: HashMap<String, String>,
#[serde(default)]
lint: Option<LintConfig>,
#[serde(default)]
cache_dir: Option<String>,
#[serde(default)]
exclude: Option<Vec<String>>,
#[serde(default)]
extend_exclude: Vec<String>,
#[serde(default)]
include: Option<Vec<String>>,
#[serde(default)]
extend_include: Vec<String>,
#[serde(default)]
flavor_overrides: HashMap<String, Flavor>,
}
fn default_line_width() -> usize {
80
}
fn default_external_max_parallel() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.clamp(1, 8)
}
fn default_blank_lines() -> BlankLines {
BlankLines::Collapse
}
fn default_tab_width() -> usize {
4
}
fn resolve_formatter_name(
name: &str,
formatter_definitions: &HashMap<String, FormatterDefinition>,
) -> Result<FormatterConfig, String> {
if let Some(definition) = formatter_definitions.get(name) {
if definition.preset.is_some() {
return Err(format!(
"Formatter '{}': 'preset' field not allowed in named definitions. Use [formatters] mapping instead (e.g., `lang = \"{}\"`).",
name, name
));
}
let preset = get_formatter_preset(name);
match (preset, &definition.cmd) {
(Some(mut base_config), _) => {
if let Some(cmd) = &definition.cmd {
base_config.cmd = cmd.clone();
}
if let Some(args) = &definition.args {
base_config.args = args.clone();
}
if let Some(stdin) = definition.stdin {
base_config.stdin = stdin;
}
apply_arg_modifiers(&mut base_config.args, definition);
Ok(base_config)
}
(None, Some(cmd)) => {
let mut args = definition.args.clone().unwrap_or_default();
apply_arg_modifiers(&mut args, definition);
Ok(FormatterConfig {
cmd: cmd.clone(),
args,
enabled: true,
stdin: definition.stdin.unwrap_or(true),
})
}
(None, None) => Err(format!(
"Formatter '{}': must specify 'cmd' field (not a known preset)",
name
)),
}
} else {
get_formatter_preset(name).ok_or_else(|| {
format!(
"Unknown formatter '{}': not a named definition or built-in preset. \
Define it in [formatters.{}] section or use a known preset.",
name, name
)
})
}
}
fn apply_arg_modifiers(args: &mut Vec<String>, definition: &FormatterDefinition) {
if let Some(prepend) = &definition.prepend_args {
let mut new_args = prepend.clone();
new_args.append(args);
*args = new_args;
}
if let Some(append) = &definition.append_args {
args.extend_from_slice(append);
}
}
fn resolve_language_formatters(
lang: &str,
value: &FormatterValue,
formatter_definitions: &HashMap<String, FormatterDefinition>,
) -> Result<Vec<FormatterConfig>, String> {
let formatter_names = match value {
FormatterValue::Single(name) => vec![name.as_str()],
FormatterValue::Multiple(names) => names.iter().map(|s| s.as_str()).collect(),
};
formatter_names
.into_iter()
.map(|name| {
if !formatter_definitions.contains_key(name) {
validate_formatter_language_for_preset(lang, name)?;
}
resolve_formatter_name(name, formatter_definitions)
.map_err(|e| format!("Language '{}': {}", lang, e))
})
.collect()
}
impl RawConfig {
fn finalize(self) -> Config {
let resolved_pandoc_compat = self.pandoc_compat.unwrap_or_default();
let has_deprecated_fields = self.wrap.is_some()
|| self.math_indent != 0
|| self.math_delimiter_style != MathDelimiterStyle::default()
|| self.blank_lines != default_blank_lines()
|| self.tab_stops != TabStopMode::Normalize
|| self.tab_width != default_tab_width();
if has_deprecated_fields && self.format_section.is_none() && self.style.is_none() {
eprintln!(
"Warning: top-level style fields (wrap, math-indent, etc.) \
are deprecated. Please move them under [format] section. \
See documentation for the new format."
);
}
let style = if let Some(format_config) = self.format_section {
if self.style.is_some() {
eprintln!(
"Warning: Both [format] and deprecated [style] sections found. \
Using [format] section."
);
}
if has_deprecated_fields {
eprintln!(
"Warning: Both [format] section and top-level style fields found. \
Using [format] section and ignoring top-level fields."
);
}
format_config
} else if let Some(style_config) = self.style {
eprintln!("Warning: [style] section is deprecated. Please use [format] instead.");
if has_deprecated_fields {
eprintln!(
"Warning: Both deprecated [style] section and top-level style fields found. \
Using [style] section and ignoring top-level fields."
);
}
style_config
} else {
StyleConfig {
wrap: self.wrap.or(Some(WrapMode::Reflow)),
blank_lines: self.blank_lines,
math_delimiter_style: self.math_delimiter_style,
math_indent: self.math_indent,
tab_stops: self.tab_stops,
tab_width: self.tab_width,
built_in_greedy_wrap: true,
}
};
Config {
extensions: super::resolve_extensions_for_flavor(self.extensions.as_ref(), self.flavor),
line_ending: self.line_ending.or(Some(LineEnding::Auto)),
flavor: self.flavor,
line_width: self.line_width,
wrap: style.wrap,
blank_lines: style.blank_lines,
math_delimiter_style: style.math_delimiter_style,
math_indent: style.math_indent,
tab_stops: style.tab_stops,
tab_width: style.tab_width,
formatters: resolve_formatters(self.formatters),
linters: self.linters,
lint: self.lint.unwrap_or_default().normalize(),
cache_dir: self.cache_dir,
external_max_parallel: self
.external_max_parallel
.unwrap_or_else(default_external_max_parallel),
parser: resolved_pandoc_compat,
built_in_greedy_wrap: style.built_in_greedy_wrap,
exclude: self.exclude,
extend_exclude: self.extend_exclude,
include: self.include,
extend_include: self.extend_include,
flavor_overrides: self.flavor_overrides,
}
}
}
fn resolve_formatters(
raw_formatters: Option<toml::Value>,
) -> HashMap<String, Vec<FormatterConfig>> {
let Some(value) = raw_formatters else {
return HashMap::new();
};
let toml::Value::Table(table) = value else {
eprintln!("Warning: Invalid formatters configuration - expected table");
return HashMap::new();
};
let has_string_or_array = table
.values()
.any(|v| matches!(v, toml::Value::String(_) | toml::Value::Array(_)));
if has_string_or_array {
resolve_new_format_formatters(table)
} else {
resolve_old_format_formatters(table)
}
}
fn resolve_new_format_formatters(
table: toml::map::Map<String, toml::Value>,
) -> HashMap<String, Vec<FormatterConfig>> {
let mut mappings = HashMap::new();
let mut definitions = HashMap::new();
for (key, value) in table {
match &value {
toml::Value::String(_) | toml::Value::Array(_) => {
let formatter_value: Result<FormatterValue, _> = value.try_into();
match formatter_value {
Ok(fv) => {
mappings.insert(key, fv);
}
Err(e) => {
eprintln!("Error parsing formatter value for '{}': {}", key, e);
}
}
}
toml::Value::Table(_) => {
let definition: Result<FormatterDefinition, _> = value.try_into();
match definition {
Ok(def) => {
definitions.insert(key, def);
}
Err(e) => {
eprintln!("Error parsing formatter definition '{}': {}", key, e);
}
}
}
_ => {
eprintln!(
"Warning: Invalid formatter entry '{}' - must be string, array, or table",
key
);
}
}
}
let mut resolved = HashMap::new();
for (lang, value) in mappings {
match resolve_language_formatters(&lang, &value, &definitions) {
Ok(configs) if !configs.is_empty() => {
resolved.insert(lang, configs);
}
Ok(_) => {} Err(e) => {
eprintln!("Error resolving formatters for language '{}': {}", lang, e);
eprintln!("Skipping formatter for '{}'", lang);
}
}
}
resolved
}
fn resolve_old_format_formatters(
table: toml::map::Map<String, toml::Value>,
) -> HashMap<String, Vec<FormatterConfig>> {
eprintln!(
"Warning: Old formatter configuration format detected. \
Please migrate to the new format with [formatters] section. \
See documentation for the new format."
);
let mut resolved = HashMap::new();
for (lang, value) in table {
let definition: Result<FormatterDefinition, _> = value.try_into();
match definition {
Ok(def) => {
if def.enabled == Some(false) {
continue;
}
match resolve_old_format_definition(&lang, &def) {
Ok(config) => {
resolved.insert(lang, vec![config]);
}
Err(e) => {
eprintln!("Error in old formatter config for '{}': {}", lang, e);
eprintln!("Skipping formatter for '{}'", lang);
}
}
}
Err(e) => {
eprintln!("Error parsing old formatter config for '{}': {}", lang, e);
}
}
}
resolved
}
fn resolve_old_format_definition(
_lang: &str,
definition: &FormatterDefinition,
) -> Result<FormatterConfig, String> {
if definition.preset.is_some() && definition.cmd.is_some() {
return Err("'preset' and 'cmd' are mutually exclusive".to_string());
}
if let Some(preset_name) = &definition.preset {
let preset = get_formatter_preset(preset_name).ok_or_else(|| {
let available = formatter_preset_names().join(", ");
format!(
"Unknown formatter preset '{}'. Available presets: {}",
preset_name, available
)
})?;
let mut args = definition.args.clone().unwrap_or(preset.args);
apply_arg_modifiers(&mut args, definition);
Ok(FormatterConfig {
cmd: preset.cmd,
args,
enabled: true, stdin: preset.stdin,
})
} else if let Some(cmd) = &definition.cmd {
let mut args = definition.args.clone().unwrap_or_default();
apply_arg_modifiers(&mut args, definition);
Ok(FormatterConfig {
cmd: cmd.clone(),
args,
enabled: true,
stdin: definition.stdin.unwrap_or(true),
})
} else {
Err("must specify either 'preset' or 'cmd'".to_string())
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub flavor: Flavor,
pub extensions: Extensions,
pub line_ending: Option<LineEnding>,
pub line_width: usize,
pub math_indent: usize,
pub math_delimiter_style: MathDelimiterStyle,
pub tab_stops: TabStopMode,
pub tab_width: usize,
pub wrap: Option<WrapMode>,
pub blank_lines: BlankLines,
pub formatters: HashMap<String, Vec<FormatterConfig>>,
pub linters: HashMap<String, String>,
pub external_max_parallel: usize,
pub parser: PandocCompat,
pub lint: LintConfig,
pub cache_dir: Option<String>,
pub built_in_greedy_wrap: bool,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Vec<String>,
pub include: Option<Vec<String>>,
pub extend_include: Vec<String>,
pub flavor_overrides: HashMap<String, Flavor>,
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
RawConfig::deserialize(deserializer).map(|raw| raw.finalize())
}
}
impl Default for Config {
fn default() -> Self {
let flavor = Flavor::default();
Self {
flavor,
extensions: Extensions::for_flavor(flavor),
line_ending: Some(LineEnding::Auto),
line_width: 80,
math_indent: 0,
math_delimiter_style: MathDelimiterStyle::default(),
tab_stops: TabStopMode::Normalize,
tab_width: 4,
wrap: Some(WrapMode::Reflow),
blank_lines: BlankLines::Collapse,
formatters: HashMap::new(), linters: HashMap::new(), external_max_parallel: default_external_max_parallel(),
parser: PandocCompat::default(),
lint: LintConfig::default(),
cache_dir: None,
built_in_greedy_wrap: true,
exclude: None,
extend_exclude: Vec::new(),
include: None,
extend_include: Vec::new(),
flavor_overrides: HashMap::new(),
}
}
}
impl Config {
pub fn parser_options(&self) -> ParserOptions {
ParserOptions {
flavor: self.flavor,
extensions: self.extensions.clone(),
pandoc_compat: self.parser,
}
}
}
#[derive(Default, Clone)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn math_indent(mut self, indent: usize) -> Self {
self.config.math_indent = indent;
self
}
pub fn tab_stops(mut self, mode: TabStopMode) -> Self {
self.config.tab_stops = mode;
self
}
pub fn tab_width(mut self, width: usize) -> Self {
self.config.tab_width = width;
self
}
pub fn line_width(mut self, width: usize) -> Self {
self.config.line_width = width;
self
}
pub fn line_ending(mut self, ending: LineEnding) -> Self {
self.config.line_ending = Some(ending);
self
}
pub fn blank_lines(mut self, mode: BlankLines) -> Self {
self.config.blank_lines = mode;
self
}
pub fn build(self) -> Config {
self.config
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum WrapMode {
Preserve,
Reflow,
Sentence,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum LineEnding {
Auto,
Lf,
Crlf,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum BlankLines {
Preserve,
Collapse,
}