Skip to content

Commit 014c7c5

Browse files
authored
feat(fmt): Configurable autoformat with FormatConfig (#95)
Fixes: #85
1 parent 876a427 commit 014c7c5

File tree

4 files changed

+283
-76
lines changed

4 files changed

+283
-76
lines changed

src/document.rs

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use miette::SourceSpan;
33
use std::fmt::Display;
44

5-
use crate::{KdlNode, KdlParseFailure, KdlValue};
5+
use crate::{FormatConfig, KdlNode, KdlParseFailure, KdlValue};
66

77
/// Represents a KDL
88
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
@@ -232,12 +232,33 @@ impl KdlDocument {
232232
/// Auto-formats this Document, making everything nice while preserving
233233
/// comments.
234234
pub fn autoformat(&mut self) {
235-
self.autoformat_impl(0, false);
235+
self.autoformat_config(&FormatConfig::default());
236236
}
237237

238238
/// Formats the document and removes all comments from the document.
239239
pub fn autoformat_no_comments(&mut self) {
240-
self.autoformat_impl(0, true);
240+
self.autoformat_config(&FormatConfig {
241+
no_comments: true,
242+
..Default::default()
243+
});
244+
}
245+
246+
/// Formats the document according to `config`.
247+
pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) {
248+
if let Some(KdlDocumentFormat { leading, .. }) = (&mut *self).format_mut() {
249+
crate::fmt::autoformat_leading(leading, config);
250+
}
251+
let mut has_nodes = false;
252+
for node in &mut (&mut *self).nodes {
253+
has_nodes = true;
254+
node.autoformat_config(config);
255+
}
256+
if let Some(KdlDocumentFormat { trailing, .. }) = (&mut *self).format_mut() {
257+
crate::fmt::autoformat_trailing(trailing, config.no_comments);
258+
if !has_nodes {
259+
trailing.push('\n');
260+
}
261+
};
241262
}
242263

243264
// TODO(@zkat): These should all be moved into the query module itself,
@@ -326,23 +347,6 @@ impl Display for KdlDocument {
326347
}
327348

328349
impl KdlDocument {
329-
pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) {
330-
if let Some(KdlDocumentFormat { leading, .. }) = self.format_mut() {
331-
crate::fmt::autoformat_leading(leading, indent, no_comments);
332-
}
333-
let mut has_nodes = false;
334-
for node in &mut self.nodes {
335-
has_nodes = true;
336-
node.autoformat_impl(indent, no_comments);
337-
}
338-
if let Some(KdlDocumentFormat { trailing, .. }) = self.format_mut() {
339-
crate::fmt::autoformat_trailing(trailing, no_comments);
340-
if !has_nodes {
341-
trailing.push('\n');
342-
}
343-
}
344-
}
345-
346350
pub(crate) fn stringify(
347351
&self,
348352
f: &mut std::fmt::Formatter<'_>,
@@ -648,6 +652,65 @@ foo 1 bar=0xdeadbeef {
648652
Ok(())
649653
}
650654

655+
#[test]
656+
fn simple_autoformat_two_spaces() -> miette::Result<()> {
657+
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
658+
KdlDocument::autoformat_config(
659+
&mut doc,
660+
&FormatConfig {
661+
indent: " ",
662+
..Default::default()
663+
},
664+
);
665+
assert_eq!(
666+
doc.to_string(),
667+
r#"a {
668+
b {
669+
c {
670+
671+
}
672+
}
673+
}
674+
"#
675+
);
676+
Ok(())
677+
}
678+
679+
#[test]
680+
fn simple_autoformat_single_tabs() -> miette::Result<()> {
681+
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
682+
KdlDocument::autoformat_config(
683+
&mut doc,
684+
&FormatConfig {
685+
indent: "\t",
686+
..Default::default()
687+
},
688+
);
689+
assert_eq!(doc.to_string(), "a {\n\tb {\n\t\tc {\n\n\t\t}\n\t}\n}\n");
690+
Ok(())
691+
}
692+
693+
#[test]
694+
fn simple_autoformat_no_comments() -> miette::Result<()> {
695+
let mut doc: KdlDocument =
696+
"// a comment\na {\n// another comment\n b { c { // another comment\n }; }; }"
697+
.parse()
698+
.unwrap();
699+
KdlDocument::autoformat_no_comments(&mut doc);
700+
assert_eq!(
701+
doc.to_string(),
702+
r#"a {
703+
b {
704+
c {
705+
706+
}
707+
}
708+
}
709+
"#
710+
);
711+
Ok(())
712+
}
713+
651714
#[cfg(feature = "span")]
652715
fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
653716
for node in doc.nodes() {

src/fmt.rs

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,131 @@
11
use std::fmt::Write as _;
22

3-
pub(crate) fn autoformat_leading(leading: &mut String, indent: usize, no_comments: bool) {
3+
/// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`)
4+
/// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`).
5+
#[non_exhaustive]
6+
#[derive(Debug)]
7+
pub struct FormatConfig<'a> {
8+
/// How deeply to indent the overall node or document,
9+
/// in repetitions of [`indent`](`FormatConfig::indent`).
10+
/// Defaults to `0`.
11+
pub indent_level: usize,
12+
13+
/// The indentation to use at each level. Defaults to four spaces.
14+
pub indent: &'a str,
15+
16+
/// Whether to remove comments. Defaults to `false`.
17+
pub no_comments: bool,
18+
}
19+
20+
/// See field documentation for defaults.
21+
impl Default for FormatConfig<'_> {
22+
fn default() -> Self {
23+
Self::builder().build()
24+
}
25+
}
26+
27+
impl FormatConfig<'_> {
28+
/// Creates a new [`FormatConfigBuilder`] with default configuration.
29+
pub const fn builder() -> FormatConfigBuilder<'static> {
30+
FormatConfigBuilder::new()
31+
}
32+
}
33+
34+
/// A [`FormatConfig`] builder.
35+
///
36+
/// Note that setters can be repeated.
37+
#[derive(Debug, Default)]
38+
pub struct FormatConfigBuilder<'a>(FormatConfig<'a>);
39+
40+
impl<'a> FormatConfigBuilder<'a> {
41+
/// Creates a new [`FormatConfig`] builder with default configuration.
42+
pub const fn new() -> Self {
43+
FormatConfigBuilder(FormatConfig {
44+
indent_level: 0,
45+
indent: " ",
46+
no_comments: false,
47+
})
48+
}
49+
50+
/// How deeply to indent the overall node or document,
51+
/// in repetitions of [`indent`](`FormatConfig::indent`).
52+
/// Defaults to `0` iff not specified.
53+
pub const fn maybe_indent_level(mut self, indent_level: Option<usize>) -> Self {
54+
if let Some(indent_level) = indent_level {
55+
self.0.indent_level = indent_level;
56+
}
57+
self
58+
}
59+
60+
/// How deeply to indent the overall node or document,
61+
/// in repetitions of [`indent`](`FormatConfig::indent`).
62+
/// Defaults to `0` iff not specified.
63+
pub const fn indent_level(mut self, indent_level: usize) -> Self {
64+
self.0.indent_level = indent_level;
65+
self
66+
}
67+
68+
/// The indentation to use at each level.
69+
/// Defaults to four spaces iff not specified.
70+
pub const fn maybe_indent<'b, 'c>(self, indent: Option<&'b str>) -> FormatConfigBuilder<'c>
71+
where
72+
'a: 'b,
73+
'b: 'c,
74+
{
75+
if let Some(indent) = indent {
76+
self.indent(indent)
77+
} else {
78+
self
79+
}
80+
}
81+
82+
/// The indentation to use at each level.
83+
/// Defaults to four spaces if not specified.
84+
pub const fn indent<'b>(self, indent: &'b str) -> FormatConfigBuilder<'b> {
85+
FormatConfigBuilder(FormatConfig { indent, ..self.0 })
86+
}
87+
88+
/// Whether to remove comments.
89+
/// Defaults to `false` iff not specified.
90+
pub const fn maybe_no_comments(mut self, no_comments: Option<bool>) -> Self {
91+
if let Some(no_comments) = no_comments {
92+
self.0.no_comments = no_comments;
93+
}
94+
self
95+
}
96+
97+
/// Whether to remove comments.
98+
/// Defaults to `false` iff not specified.
99+
pub const fn no_comments(mut self, no_comments: bool) -> Self {
100+
self.0.no_comments = no_comments;
101+
self
102+
}
103+
104+
/// Builds the [`FormatConfig`].
105+
pub const fn build(self) -> FormatConfig<'a> {
106+
self.0
107+
}
108+
}
109+
110+
pub(crate) fn autoformat_leading(leading: &mut String, config: &FormatConfig<'_>) {
4111
let mut result = String::new();
5-
if !no_comments {
112+
if !config.no_comments {
6113
let input = leading.trim();
7114
if !input.is_empty() {
8115
for line in input.lines() {
9116
let trimmed = line.trim();
10117
if !trimmed.is_empty() {
11-
writeln!(result, "{:indent$}{}", "", trimmed, indent = indent).unwrap();
118+
for _ in 0..config.indent_level {
119+
result.push_str(config.indent);
120+
}
121+
writeln!(result, "{}", trimmed).unwrap();
12122
}
13123
}
14124
}
15125
}
16-
write!(result, "{:indent$}", "", indent = indent).unwrap();
126+
for _ in 0..config.indent_level {
127+
result.push_str(config.indent);
128+
}
17129
*leading = result;
18130
}
19131

@@ -33,3 +145,26 @@ pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
33145
}
34146
*decor = result;
35147
}
148+
149+
#[cfg(test)]
150+
mod test {
151+
use super::*;
152+
153+
#[test]
154+
fn builder() -> miette::Result<()> {
155+
let built = FormatConfig::builder()
156+
.indent_level(12)
157+
.indent(" \t")
158+
.no_comments(true)
159+
.build();
160+
assert!(matches!(
161+
built,
162+
FormatConfig {
163+
indent_level: 12,
164+
indent: " \t",
165+
no_comments: true,
166+
}
167+
));
168+
Ok(())
169+
}
170+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
pub use document::*;
176176
pub use entry::*;
177177
pub use error::*;
178+
pub use fmt::*;
178179
pub use identifier::*;
179180
pub use node::*;
180181
// pub use query::*;

0 commit comments

Comments
 (0)