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///
39/// Only takes effect when [`Config::dangle_parens`] is `true`.
40/// Controls where `)` is placed when a call wraps onto multiple lines:
41///
42/// ```cmake
43/// # Prefix / Close — `)` at the command-name column (tracks block depth):
44/// target_link_libraries(
45/// mylib PUBLIC dep1
46/// )
47///
48/// # Open — `)` at the opening-paren column:
49/// target_link_libraries(
50/// mylib PUBLIC dep1
51/// )
52/// ```
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[serde(rename_all = "lowercase")]
55pub enum DangleAlign {
56 /// Align with the start of the command name.
57 #[default]
58 Prefix,
59 /// Align with the opening paren column.
60 Open,
61 /// No extra indent (flush with current indent level).
62 Close,
63}
64
65/// Full formatter configuration.
66///
67/// Construct [`Config::default`] and set fields as needed before passing it to
68/// [`format_source`](crate::format_source) or related functions.
69///
70/// ```
71/// use cmakefmt::{Config, CaseStyle, DangleAlign};
72///
73/// let config = Config {
74/// line_width: 100,
75/// command_case: CaseStyle::Lower,
76/// dangle_parens: true,
77/// dangle_align: DangleAlign::Open,
78/// ..Config::default()
79/// };
80/// ```
81///
82/// # Defaults
83///
84/// | Field | Default |
85/// |-------|---------|
86/// | `line_width` | `80` |
87/// | `tab_size` | `2` |
88/// | `use_tabchars` | `false` |
89/// | `max_empty_lines` | `1` |
90/// | `command_case` | [`CaseStyle::Lower`] |
91/// | `keyword_case` | [`CaseStyle::Upper`] |
92/// | `dangle_parens` | `false` |
93/// | `dangle_align` | [`DangleAlign::Prefix`] |
94/// | `enable_markup` | `true` |
95/// | `reflow_comments` | `false` |
96/// | `first_comment_is_literal` | `true` |
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Config {
99 // ── Layout ──────────────────────────────────────────────────────────
100 /// Maximum rendered line width before wrapping is attempted.
101 pub line_width: usize,
102 /// Number of spaces that make up one indentation level when
103 /// [`Self::use_tabchars`] is `false`.
104 pub tab_size: usize,
105 /// Emit tab characters for indentation instead of spaces.
106 pub use_tabchars: bool,
107 /// Maximum number of consecutive empty lines to preserve.
108 pub max_empty_lines: usize,
109 /// Maximum number of wrapped lines tolerated before switching to a more
110 /// vertical layout.
111 pub max_lines_hwrap: usize,
112 /// Maximum number of positional arguments to keep in a hanging-wrap layout
113 /// before going vertical.
114 pub max_pargs_hwrap: usize,
115 /// Maximum number of keyword/flag subgroups to keep in a horizontal wrap.
116 pub max_subgroups_hwrap: usize,
117
118 // ── Parenthesis style ───────────────────────────────────────────────
119 /// Place the closing `)` on its own line when a call wraps.
120 pub dangle_parens: bool,
121 /// Alignment strategy for a dangling closing `)`.
122 pub dangle_align: DangleAlign,
123 /// Lower bound used by layout heuristics when deciding whether a command
124 /// name is short enough to prefer one style over another.
125 pub min_prefix_chars: usize,
126 /// Upper bound used by layout heuristics when deciding whether a command
127 /// name is long enough to prefer one style over another.
128 pub max_prefix_chars: usize,
129 /// Insert a space before `(` for control-flow commands such as `if`.
130 pub separate_ctrl_name_with_space: bool,
131 /// Insert a space before `(` for `function`/`macro` definitions.
132 pub separate_fn_name_with_space: bool,
133
134 // ── Casing ──────────────────────────────────────────────────────────
135 /// Output casing policy for command names.
136 pub command_case: CaseStyle,
137 /// Output casing policy for recognized keywords and flags.
138 pub keyword_case: CaseStyle,
139
140 // ── Comment markup ──────────────────────────────────────────────────
141 /// Enable markup-aware comment handling.
142 pub enable_markup: bool,
143 /// Reflow plain line comments to fit within the configured width.
144 pub reflow_comments: bool,
145 /// Preserve the first comment block in a file literally.
146 pub first_comment_is_literal: bool,
147 /// Regex for comments that should never be reflowed.
148 pub literal_comment_pattern: String,
149 /// Preferred bullet character when normalizing list markup.
150 pub bullet_char: String,
151 /// Preferred enumeration punctuation when normalizing numbered list markup.
152 pub enum_char: String,
153 /// Regex describing fenced literal comment blocks.
154 pub fence_pattern: String,
155 /// Regex describing ruler-style comments.
156 pub ruler_pattern: String,
157 /// Minimum ruler length before a `#-----` style line is treated as a ruler.
158 pub hashruler_min_length: usize,
159 /// Normalize ruler comments when markup handling is enabled.
160 pub canonicalize_hashrulers: bool,
161
162 // ── Per-command overrides ────────────────────────────────────────────
163 /// Per-command configuration overrides keyed by lowercase command name.
164 pub per_command_overrides: HashMap<String, PerCommandConfig>,
165}
166
167/// Per-command overrides. All fields are optional — only specified fields
168/// override the global config for that command.
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
170#[serde(deny_unknown_fields)]
171pub struct PerCommandConfig {
172 /// Override the command casing rule for this command only.
173 pub command_case: Option<CaseStyle>,
174 /// Override the keyword casing rule for this command only.
175 pub keyword_case: Option<CaseStyle>,
176 /// Override the line width for this command only.
177 pub line_width: Option<usize>,
178 /// Override the indentation width for this command only.
179 pub tab_size: Option<usize>,
180 /// Override dangling paren placement for this command only.
181 pub dangle_parens: Option<bool>,
182 /// Override dangling paren alignment for this command only.
183 pub dangle_align: Option<DangleAlign>,
184 /// Override the hanging-wrap positional argument threshold for this
185 /// command only.
186 #[serde(rename = "max_hanging_wrap_positional_args")]
187 pub max_pargs_hwrap: Option<usize>,
188 /// Override the hanging-wrap subgroup threshold for this command only.
189 #[serde(rename = "max_hanging_wrap_groups")]
190 pub max_subgroups_hwrap: Option<usize>,
191}
192
193impl Default for Config {
194 fn default() -> Self {
195 Self {
196 line_width: 80,
197 tab_size: 2,
198 use_tabchars: false,
199 max_empty_lines: 1,
200 max_lines_hwrap: 2,
201 max_pargs_hwrap: 6,
202 max_subgroups_hwrap: 2,
203 dangle_parens: false,
204 dangle_align: DangleAlign::Prefix,
205 min_prefix_chars: 4,
206 max_prefix_chars: 10,
207 separate_ctrl_name_with_space: false,
208 separate_fn_name_with_space: false,
209 command_case: CaseStyle::Lower,
210 keyword_case: CaseStyle::Upper,
211 enable_markup: true,
212 reflow_comments: false,
213 first_comment_is_literal: true,
214 literal_comment_pattern: String::new(),
215 bullet_char: "*".to_string(),
216 enum_char: ".".to_string(),
217 fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
218 ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
219 hashruler_min_length: 10,
220 canonicalize_hashrulers: true,
221 per_command_overrides: HashMap::new(),
222 }
223 }
224}
225
226/// CMake control-flow commands that get `separate_ctrl_name_with_space`.
227const CONTROL_FLOW_COMMANDS: &[&str] = &[
228 "if",
229 "elseif",
230 "else",
231 "endif",
232 "foreach",
233 "endforeach",
234 "while",
235 "endwhile",
236 "break",
237 "continue",
238 "return",
239 "block",
240 "endblock",
241];
242
243/// CMake function/macro definition commands that get
244/// `separate_fn_name_with_space`.
245const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
246
247impl Config {
248 /// Returns a `Config` with any per-command overrides applied for the
249 /// given command name, plus the appropriate space-before-paren setting.
250 pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
251 let lower = command_name.to_ascii_lowercase();
252 let per_cmd = self.per_command_overrides.get(&lower);
253
254 let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
255 self.separate_ctrl_name_with_space
256 } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
257 self.separate_fn_name_with_space
258 } else {
259 false
260 };
261
262 CommandConfig {
263 global: self,
264 per_cmd,
265 space_before_paren,
266 }
267 }
268
269 /// Apply the command_case rule to a command name.
270 pub fn apply_command_case(&self, name: &str) -> String {
271 apply_case(self.command_case, name)
272 }
273
274 /// Apply the keyword_case rule to a keyword token.
275 pub fn apply_keyword_case(&self, keyword: &str) -> String {
276 apply_case(self.keyword_case, keyword)
277 }
278
279 /// The indentation string (spaces or tab).
280 pub fn indent_str(&self) -> String {
281 if self.use_tabchars {
282 "\t".to_string()
283 } else {
284 " ".repeat(self.tab_size)
285 }
286 }
287}
288
289/// A resolved config for formatting a specific command, with per-command
290/// overrides already applied.
291#[derive(Debug)]
292pub struct CommandConfig<'a> {
293 /// The global configuration before per-command overrides are applied.
294 pub global: &'a Config,
295 per_cmd: Option<&'a PerCommandConfig>,
296 /// Whether this command should render a space before `(`.
297 pub space_before_paren: bool,
298}
299
300impl CommandConfig<'_> {
301 /// Effective line width for the current command.
302 pub fn line_width(&self) -> usize {
303 self.per_cmd
304 .and_then(|p| p.line_width)
305 .unwrap_or(self.global.line_width)
306 }
307
308 /// Effective indentation width for the current command.
309 pub fn tab_size(&self) -> usize {
310 self.per_cmd
311 .and_then(|p| p.tab_size)
312 .unwrap_or(self.global.tab_size)
313 }
314
315 /// Effective dangling-paren setting for the current command.
316 pub fn dangle_parens(&self) -> bool {
317 self.per_cmd
318 .and_then(|p| p.dangle_parens)
319 .unwrap_or(self.global.dangle_parens)
320 }
321
322 /// Effective dangling-paren alignment for the current command.
323 pub fn dangle_align(&self) -> DangleAlign {
324 self.per_cmd
325 .and_then(|p| p.dangle_align)
326 .unwrap_or(self.global.dangle_align)
327 }
328
329 /// Effective command casing rule for the current command.
330 pub fn command_case(&self) -> CaseStyle {
331 self.per_cmd
332 .and_then(|p| p.command_case)
333 .unwrap_or(self.global.command_case)
334 }
335
336 /// Effective keyword casing rule for the current command.
337 pub fn keyword_case(&self) -> CaseStyle {
338 self.per_cmd
339 .and_then(|p| p.keyword_case)
340 .unwrap_or(self.global.keyword_case)
341 }
342
343 /// Effective hanging-wrap positional argument threshold for the current
344 /// command.
345 pub fn max_pargs_hwrap(&self) -> usize {
346 self.per_cmd
347 .and_then(|p| p.max_pargs_hwrap)
348 .unwrap_or(self.global.max_pargs_hwrap)
349 }
350
351 /// Effective hanging-wrap subgroup threshold for the current command.
352 pub fn max_subgroups_hwrap(&self) -> usize {
353 self.per_cmd
354 .and_then(|p| p.max_subgroups_hwrap)
355 .unwrap_or(self.global.max_subgroups_hwrap)
356 }
357
358 /// Effective indentation unit for the current command.
359 pub fn indent_str(&self) -> String {
360 if self.global.use_tabchars {
361 "\t".to_string()
362 } else {
363 " ".repeat(self.tab_size())
364 }
365 }
366}
367
368fn apply_case(style: CaseStyle, s: &str) -> String {
369 match style {
370 CaseStyle::Lower => s.to_ascii_lowercase(),
371 CaseStyle::Upper => s.to_ascii_uppercase(),
372 CaseStyle::Unchanged => s.to_string(),
373 }
374}