cmakefmt/config/mod.rs
1// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Runtime formatter configuration.
6//!
7//! [`Config`] is the fully resolved in-memory configuration used by the
8//! formatter. It is built from defaults, user config files
9//! (`.cmakefmt.yaml`, `.cmakefmt.yml`, or `.cmakefmt.toml`), and CLI
10//! overrides.
11
12pub mod file;
13mod legacy;
14/// Render a commented starter config template.
15pub use file::{
16 default_config_template, default_config_template_for, render_effective_config, DumpConfigFormat,
17};
18pub use legacy::convert_legacy_config_files;
19
20use std::collections::HashMap;
21
22use serde::{Deserialize, Serialize};
23
24/// How to normalise command/keyword casing.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
26#[serde(rename_all = "lowercase")]
27pub enum CaseStyle {
28 /// Force lowercase output.
29 Lower,
30 /// Force uppercase output.
31 #[default]
32 Upper,
33 /// Preserve the original source casing.
34 Unchanged,
35}
36
37/// How to align the dangling closing paren.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
39#[serde(rename_all = "lowercase")]
40pub enum DangleAlign {
41 /// Align with the start of the command name.
42 #[default]
43 Prefix,
44 /// Align with the opening paren column.
45 Open,
46 /// No extra indent (flush with current indent level).
47 Close,
48}
49
50/// Full formatter configuration.
51///
52/// This struct is used at runtime. It is populated from defaults, supported
53/// user config files (YAML or TOML), and CLI flag overrides (highest wins).
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Config {
56 // ── Layout ──────────────────────────────────────────────────────────
57 /// Maximum rendered line width before wrapping is attempted.
58 pub line_width: usize,
59 /// Number of spaces that make up one indentation level when
60 /// [`Self::use_tabchars`] is `false`.
61 pub tab_size: usize,
62 /// Emit tab characters for indentation instead of spaces.
63 pub use_tabchars: bool,
64 /// Maximum number of consecutive empty lines to preserve.
65 pub max_empty_lines: usize,
66 /// Maximum number of wrapped lines tolerated before switching to a more
67 /// vertical layout.
68 pub max_lines_hwrap: usize,
69 /// Maximum number of positional arguments to keep in a hanging-wrap layout
70 /// before going vertical.
71 pub max_pargs_hwrap: usize,
72 /// Maximum number of keyword/flag subgroups to keep in a horizontal wrap.
73 pub max_subgroups_hwrap: usize,
74
75 // ── Parenthesis style ───────────────────────────────────────────────
76 /// Place the closing `)` on its own line when a call wraps.
77 pub dangle_parens: bool,
78 /// Alignment strategy for a dangling closing `)`.
79 pub dangle_align: DangleAlign,
80 /// Lower bound used by layout heuristics when deciding whether a command
81 /// name is short enough to prefer one style over another.
82 pub min_prefix_chars: usize,
83 /// Upper bound used by layout heuristics when deciding whether a command
84 /// name is long enough to prefer one style over another.
85 pub max_prefix_chars: usize,
86 /// Insert a space before `(` for control-flow commands such as `if`.
87 pub separate_ctrl_name_with_space: bool,
88 /// Insert a space before `(` for `function`/`macro` definitions.
89 pub separate_fn_name_with_space: bool,
90
91 // ── Casing ──────────────────────────────────────────────────────────
92 /// Output casing policy for command names.
93 pub command_case: CaseStyle,
94 /// Output casing policy for recognized keywords and flags.
95 pub keyword_case: CaseStyle,
96
97 // ── Comment markup ──────────────────────────────────────────────────
98 /// Enable markup-aware comment handling.
99 pub enable_markup: bool,
100 /// Reflow plain line comments to fit within the configured width.
101 pub reflow_comments: bool,
102 /// Preserve the first comment block in a file literally.
103 pub first_comment_is_literal: bool,
104 /// Regex for comments that should never be reflowed.
105 pub literal_comment_pattern: String,
106 /// Preferred bullet character when normalizing list markup.
107 pub bullet_char: String,
108 /// Preferred enumeration punctuation when normalizing numbered list markup.
109 pub enum_char: String,
110 /// Regex describing fenced literal comment blocks.
111 pub fence_pattern: String,
112 /// Regex describing ruler-style comments.
113 pub ruler_pattern: String,
114 /// Minimum ruler length before a `#-----` style line is treated as a ruler.
115 pub hashruler_min_length: usize,
116 /// Normalize ruler comments when markup handling is enabled.
117 pub canonicalize_hashrulers: bool,
118
119 // ── Per-command overrides ────────────────────────────────────────────
120 /// Per-command configuration overrides keyed by lowercase command name.
121 pub per_command_overrides: HashMap<String, PerCommandConfig>,
122}
123
124/// Per-command overrides. All fields are optional — only specified fields
125/// override the global config for that command.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
127#[serde(deny_unknown_fields)]
128pub struct PerCommandConfig {
129 /// Override the command casing rule for this command only.
130 pub command_case: Option<CaseStyle>,
131 /// Override the keyword casing rule for this command only.
132 pub keyword_case: Option<CaseStyle>,
133 /// Override the line width for this command only.
134 pub line_width: Option<usize>,
135 /// Override the indentation width for this command only.
136 pub tab_size: Option<usize>,
137 /// Override dangling paren placement for this command only.
138 pub dangle_parens: Option<bool>,
139 /// Override dangling paren alignment for this command only.
140 pub dangle_align: Option<DangleAlign>,
141 /// Override the hanging-wrap positional argument threshold for this
142 /// command only.
143 #[serde(rename = "max_hanging_wrap_positional_args")]
144 pub max_pargs_hwrap: Option<usize>,
145 /// Override the hanging-wrap subgroup threshold for this command only.
146 #[serde(rename = "max_hanging_wrap_groups")]
147 pub max_subgroups_hwrap: Option<usize>,
148}
149
150impl Default for Config {
151 fn default() -> Self {
152 Self {
153 line_width: 80,
154 tab_size: 2,
155 use_tabchars: false,
156 max_empty_lines: 1,
157 max_lines_hwrap: 2,
158 max_pargs_hwrap: 6,
159 max_subgroups_hwrap: 2,
160 dangle_parens: false,
161 dangle_align: DangleAlign::Prefix,
162 min_prefix_chars: 4,
163 max_prefix_chars: 10,
164 separate_ctrl_name_with_space: false,
165 separate_fn_name_with_space: false,
166 command_case: CaseStyle::Lower,
167 keyword_case: CaseStyle::Upper,
168 enable_markup: true,
169 reflow_comments: false,
170 first_comment_is_literal: true,
171 literal_comment_pattern: String::new(),
172 bullet_char: "*".to_string(),
173 enum_char: ".".to_string(),
174 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
175 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
176 hashruler_min_length: 10,
177 canonicalize_hashrulers: true,
178 per_command_overrides: HashMap::new(),
179 }
180 }
181}
182
183/// CMake control-flow commands that get `separate_ctrl_name_with_space`.
184const CONTROL_FLOW_COMMANDS: &[&str] = &[
185 "if",
186 "elseif",
187 "else",
188 "endif",
189 "foreach",
190 "endforeach",
191 "while",
192 "endwhile",
193 "break",
194 "continue",
195 "return",
196 "block",
197 "endblock",
198];
199
200/// CMake function/macro definition commands that get
201/// `separate_fn_name_with_space`.
202const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
203
204impl Config {
205 /// Returns a `Config` with any per-command overrides applied for the
206 /// given command name, plus the appropriate space-before-paren setting.
207 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
208 let lower = command_name.to_ascii_lowercase();
209 let per_cmd = self.per_command_overrides.get(&lower);
210
211 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
212 self.separate_ctrl_name_with_space
213 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
214 self.separate_fn_name_with_space
215 } else {
216 false
217 };
218
219 CommandConfig {
220 global: self,
221 per_cmd,
222 space_before_paren,
223 }
224 }
225
226 /// Apply the command_case rule to a command name.
227 pub fn apply_command_case(&self, name: &str) -> String {
228 apply_case(self.command_case, name)
229 }
230
231 /// Apply the keyword_case rule to a keyword token.
232 pub fn apply_keyword_case(&self, keyword: &str) -> String {
233 apply_case(self.keyword_case, keyword)
234 }
235
236 /// The indentation string (spaces or tab).
237 pub fn indent_str(&self) -> String {
238 if self.use_tabchars {
239 "\t".to_string()
240 } else {
241 " ".repeat(self.tab_size)
242 }
243 }
244}
245
246/// A resolved config for formatting a specific command, with per-command
247/// overrides already applied.
248#[derive(Debug)]
249pub struct CommandConfig<'a> {
250 /// The global configuration before per-command overrides are applied.
251 pub global: &'a Config,
252 per_cmd: Option<&'a PerCommandConfig>,
253 /// Whether this command should render a space before `(`.
254 pub space_before_paren: bool,
255}
256
257impl CommandConfig<'_> {
258 /// Effective line width for the current command.
259 pub fn line_width(&self) -> usize {
260 self.per_cmd
261 .and_then(|p| p.line_width)
262 .unwrap_or(self.global.line_width)
263 }
264
265 /// Effective indentation width for the current command.
266 pub fn tab_size(&self) -> usize {
267 self.per_cmd
268 .and_then(|p| p.tab_size)
269 .unwrap_or(self.global.tab_size)
270 }
271
272 /// Effective dangling-paren setting for the current command.
273 pub fn dangle_parens(&self) -> bool {
274 self.per_cmd
275 .and_then(|p| p.dangle_parens)
276 .unwrap_or(self.global.dangle_parens)
277 }
278
279 /// Effective dangling-paren alignment for the current command.
280 pub fn dangle_align(&self) -> DangleAlign {
281 self.per_cmd
282 .and_then(|p| p.dangle_align)
283 .unwrap_or(self.global.dangle_align)
284 }
285
286 /// Effective command casing rule for the current command.
287 pub fn command_case(&self) -> CaseStyle {
288 self.per_cmd
289 .and_then(|p| p.command_case)
290 .unwrap_or(self.global.command_case)
291 }
292
293 /// Effective keyword casing rule for the current command.
294 pub fn keyword_case(&self) -> CaseStyle {
295 self.per_cmd
296 .and_then(|p| p.keyword_case)
297 .unwrap_or(self.global.keyword_case)
298 }
299
300 /// Effective hanging-wrap positional argument threshold for the current
301 /// command.
302 pub fn max_pargs_hwrap(&self) -> usize {
303 self.per_cmd
304 .and_then(|p| p.max_pargs_hwrap)
305 .unwrap_or(self.global.max_pargs_hwrap)
306 }
307
308 /// Effective hanging-wrap subgroup threshold for the current command.
309 pub fn max_subgroups_hwrap(&self) -> usize {
310 self.per_cmd
311 .and_then(|p| p.max_subgroups_hwrap)
312 .unwrap_or(self.global.max_subgroups_hwrap)
313 }
314
315 /// Effective indentation unit for the current command.
316 pub fn indent_str(&self) -> String {
317 if self.global.use_tabchars {
318 "\t".to_string()
319 } else {
320 " ".repeat(self.tab_size())
321 }
322 }
323}
324
325fn apply_case(style: CaseStyle, s: &str) -> String {
326 match style {
327 CaseStyle::Lower => s.to_ascii_lowercase(),
328 CaseStyle::Upper => s.to_ascii_uppercase(),
329 CaseStyle::Unchanged => s.to_string(),
330 }
331}