#[derive(Debug, Clone)]
pub struct FormatConfig {
pub max_line_length: usize,
pub indent_width: usize,
pub use_tabs: bool,
pub tab_width: usize,
pub trailing_comma: TrailingComma,
pub newline: NewlineStyle,
pub brace_style: BraceStyle,
pub import_style: ImportStyle,
pub array_style: ArrayStyle,
pub fn_call_style: FnCallStyle,
pub format_doc_comments: bool,
pub format_strings: bool,
pub normalize_spacing: bool,
pub trim_trailing_whitespace: bool,
pub final_newline: bool,
pub blank_lines_before_items: usize,
pub max_blank_lines: usize,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
max_line_length: 100,
indent_width: 4,
use_tabs: false,
tab_width: 4,
trailing_comma: TrailingComma::Multiline,
newline: NewlineStyle::Unix,
brace_style: BraceStyle::SameLine,
import_style: ImportStyle::Merged,
array_style: ArrayStyle::Visual,
fn_call_style: FnCallStyle::Visual,
format_doc_comments: true,
format_strings: false,
normalize_spacing: true,
trim_trailing_whitespace: true,
final_newline: true,
blank_lines_before_items: 1,
max_blank_lines: 2,
}
}
}
impl FormatConfig {
pub fn compact() -> Self {
Self {
max_line_length: 120,
indent_width: 2,
trailing_comma: TrailingComma::Never,
blank_lines_before_items: 0,
max_blank_lines: 1,
..Default::default()
}
}
pub fn wide() -> Self {
Self {
max_line_length: 80,
indent_width: 4,
trailing_comma: TrailingComma::Always,
blank_lines_before_items: 2,
max_blank_lines: 3,
..Default::default()
}
}
pub fn indent_str(&self) -> String {
if self.use_tabs {
"\t".to_string()
} else {
" ".repeat(self.indent_width)
}
}
pub fn indent_at(&self, depth: usize) -> String {
self.indent_str().repeat(depth)
}
pub fn newline_str(&self) -> &'static str {
match self.newline {
NewlineStyle::Unix => "\n",
NewlineStyle::Windows => "\r\n",
NewlineStyle::Native => {
#[cfg(windows)]
{
"\r\n"
}
#[cfg(not(windows))]
{
"\n"
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrailingComma {
Never,
Always,
Multiline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NewlineStyle {
Unix,
Windows,
Native,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BraceStyle {
SameLine,
NextLine,
PreferSameLine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportStyle {
Preserve,
Merged,
Separate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayStyle {
Visual,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FnCallStyle {
Visual,
Block,
}
pub fn parse_config(content: &str) -> Result<FormatConfig, ConfigError> {
let mut config = FormatConfig::default();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
continue;
}
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() != 2 {
continue;
}
let key = parts[0].trim();
let value = parts[1].trim().trim_matches('"');
match key {
"max_line_length" => {
config.max_line_length = value
.parse()
.map_err(|_| ConfigError::InvalidValue(key.to_string(), value.to_string()))?;
}
"indent_width" => {
config.indent_width = value
.parse()
.map_err(|_| ConfigError::InvalidValue(key.to_string(), value.to_string()))?;
}
"use_tabs" => {
config.use_tabs = value == "true";
}
"tab_width" => {
config.tab_width = value
.parse()
.map_err(|_| ConfigError::InvalidValue(key.to_string(), value.to_string()))?;
}
"trailing_comma" => {
config.trailing_comma = match value {
"never" => TrailingComma::Never,
"always" => TrailingComma::Always,
"multiline" => TrailingComma::Multiline,
_ => {
return Err(ConfigError::InvalidValue(
key.to_string(),
value.to_string(),
))
}
};
}
"newline" => {
config.newline = match value {
"unix" | "lf" => NewlineStyle::Unix,
"windows" | "crlf" => NewlineStyle::Windows,
"native" => NewlineStyle::Native,
_ => {
return Err(ConfigError::InvalidValue(
key.to_string(),
value.to_string(),
))
}
};
}
"brace_style" => {
config.brace_style = match value {
"same_line" => BraceStyle::SameLine,
"next_line" => BraceStyle::NextLine,
"prefer_same_line" => BraceStyle::PreferSameLine,
_ => {
return Err(ConfigError::InvalidValue(
key.to_string(),
value.to_string(),
))
}
};
}
"import_style" => {
config.import_style = match value {
"preserve" => ImportStyle::Preserve,
"merged" => ImportStyle::Merged,
"separate" => ImportStyle::Separate,
_ => {
return Err(ConfigError::InvalidValue(
key.to_string(),
value.to_string(),
))
}
};
}
"format_doc_comments" => {
config.format_doc_comments = value == "true";
}
"trim_trailing_whitespace" => {
config.trim_trailing_whitespace = value == "true";
}
"final_newline" => {
config.final_newline = value == "true";
}
"blank_lines_before_items" => {
config.blank_lines_before_items = value
.parse()
.map_err(|_| ConfigError::InvalidValue(key.to_string(), value.to_string()))?;
}
"max_blank_lines" => {
config.max_blank_lines = value
.parse()
.map_err(|_| ConfigError::InvalidValue(key.to_string(), value.to_string()))?;
}
_ => {
}
}
}
Ok(config)
}
#[derive(Debug)]
pub enum ConfigError {
InvalidValue(String, String),
UnknownKey(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::InvalidValue(key, value) => {
write!(f, "invalid value '{}' for key '{}'", value, key)
}
ConfigError::UnknownKey(key) => {
write!(f, "unknown configuration key '{}'", key)
}
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config() {
let content = r#"
max_line_length = 80
indent_width = 2
use_tabs = false
trailing_comma = "always"
"#;
let config = parse_config(content).unwrap();
assert_eq!(config.max_line_length, 80);
assert_eq!(config.indent_width, 2);
assert!(!config.use_tabs);
assert_eq!(config.trailing_comma, TrailingComma::Always);
}
}