Skip to content

Commit

Permalink
feat(fmt): Configurable autoformat with FormatConfig (#95)
Browse files Browse the repository at this point in the history
Fixes: #85
  • Loading branch information
Tamschi authored Dec 6, 2024
1 parent 876a427 commit 014c7c5
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 76 deletions.
103 changes: 83 additions & 20 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use miette::SourceSpan;
use std::fmt::Display;

use crate::{KdlNode, KdlParseFailure, KdlValue};
use crate::{FormatConfig, KdlNode, KdlParseFailure, KdlValue};

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

/// Formats the document and removes all comments from the document.
pub fn autoformat_no_comments(&mut self) {
self.autoformat_impl(0, true);
self.autoformat_config(&FormatConfig {
no_comments: true,
..Default::default()
});
}

/// Formats the document according to `config`.
pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) {
if let Some(KdlDocumentFormat { leading, .. }) = (&mut *self).format_mut() {
crate::fmt::autoformat_leading(leading, config);
}
let mut has_nodes = false;
for node in &mut (&mut *self).nodes {
has_nodes = true;
node.autoformat_config(config);
}
if let Some(KdlDocumentFormat { trailing, .. }) = (&mut *self).format_mut() {
crate::fmt::autoformat_trailing(trailing, config.no_comments);
if !has_nodes {
trailing.push('\n');
}
};
}

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

impl KdlDocument {
pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(KdlDocumentFormat { leading, .. }) = self.format_mut() {
crate::fmt::autoformat_leading(leading, indent, no_comments);
}
let mut has_nodes = false;
for node in &mut self.nodes {
has_nodes = true;
node.autoformat_impl(indent, no_comments);
}
if let Some(KdlDocumentFormat { trailing, .. }) = self.format_mut() {
crate::fmt::autoformat_trailing(trailing, no_comments);
if !has_nodes {
trailing.push('\n');
}
}
}

pub(crate) fn stringify(
&self,
f: &mut std::fmt::Formatter<'_>,
Expand Down Expand Up @@ -648,6 +652,65 @@ foo 1 bar=0xdeadbeef {
Ok(())
}

#[test]
fn simple_autoformat_two_spaces() -> miette::Result<()> {
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
KdlDocument::autoformat_config(
&mut doc,
&FormatConfig {
indent: " ",
..Default::default()
},
);
assert_eq!(
doc.to_string(),
r#"a {
b {
c {
}
}
}
"#
);
Ok(())
}

#[test]
fn simple_autoformat_single_tabs() -> miette::Result<()> {
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
KdlDocument::autoformat_config(
&mut doc,
&FormatConfig {
indent: "\t",
..Default::default()
},
);
assert_eq!(doc.to_string(), "a {\n\tb {\n\t\tc {\n\n\t\t}\n\t}\n}\n");
Ok(())
}

#[test]
fn simple_autoformat_no_comments() -> miette::Result<()> {
let mut doc: KdlDocument =
"// a comment\na {\n// another comment\n b { c { // another comment\n }; }; }"
.parse()
.unwrap();
KdlDocument::autoformat_no_comments(&mut doc);
assert_eq!(
doc.to_string(),
r#"a {
b {
c {
}
}
}
"#
);
Ok(())
}

#[cfg(feature = "span")]
fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
for node in doc.nodes() {
Expand Down
143 changes: 139 additions & 4 deletions src/fmt.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,131 @@
use std::fmt::Write as _;

pub(crate) fn autoformat_leading(leading: &mut String, indent: usize, no_comments: bool) {
/// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`)
/// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`).
#[non_exhaustive]
#[derive(Debug)]
pub struct FormatConfig<'a> {
/// How deeply to indent the overall node or document,
/// in repetitions of [`indent`](`FormatConfig::indent`).
/// Defaults to `0`.
pub indent_level: usize,

/// The indentation to use at each level. Defaults to four spaces.
pub indent: &'a str,

/// Whether to remove comments. Defaults to `false`.
pub no_comments: bool,
}

/// See field documentation for defaults.
impl Default for FormatConfig<'_> {
fn default() -> Self {
Self::builder().build()
}
}

impl FormatConfig<'_> {
/// Creates a new [`FormatConfigBuilder`] with default configuration.
pub const fn builder() -> FormatConfigBuilder<'static> {
FormatConfigBuilder::new()
}
}

/// A [`FormatConfig`] builder.
///
/// Note that setters can be repeated.
#[derive(Debug, Default)]
pub struct FormatConfigBuilder<'a>(FormatConfig<'a>);

impl<'a> FormatConfigBuilder<'a> {
/// Creates a new [`FormatConfig`] builder with default configuration.
pub const fn new() -> Self {
FormatConfigBuilder(FormatConfig {
indent_level: 0,
indent: " ",
no_comments: false,
})
}

/// How deeply to indent the overall node or document,
/// in repetitions of [`indent`](`FormatConfig::indent`).
/// Defaults to `0` iff not specified.
pub const fn maybe_indent_level(mut self, indent_level: Option<usize>) -> Self {
if let Some(indent_level) = indent_level {
self.0.indent_level = indent_level;
}
self
}

/// How deeply to indent the overall node or document,
/// in repetitions of [`indent`](`FormatConfig::indent`).
/// Defaults to `0` iff not specified.
pub const fn indent_level(mut self, indent_level: usize) -> Self {
self.0.indent_level = indent_level;
self
}

/// The indentation to use at each level.
/// Defaults to four spaces iff not specified.
pub const fn maybe_indent<'b, 'c>(self, indent: Option<&'b str>) -> FormatConfigBuilder<'c>
where
'a: 'b,
'b: 'c,
{
if let Some(indent) = indent {
self.indent(indent)
} else {
self
}
}

/// The indentation to use at each level.
/// Defaults to four spaces if not specified.
pub const fn indent<'b>(self, indent: &'b str) -> FormatConfigBuilder<'b> {
FormatConfigBuilder(FormatConfig { indent, ..self.0 })
}

/// Whether to remove comments.
/// Defaults to `false` iff not specified.
pub const fn maybe_no_comments(mut self, no_comments: Option<bool>) -> Self {
if let Some(no_comments) = no_comments {
self.0.no_comments = no_comments;
}
self
}

/// Whether to remove comments.
/// Defaults to `false` iff not specified.
pub const fn no_comments(mut self, no_comments: bool) -> Self {
self.0.no_comments = no_comments;
self
}

/// Builds the [`FormatConfig`].
pub const fn build(self) -> FormatConfig<'a> {
self.0
}
}

pub(crate) fn autoformat_leading(leading: &mut String, config: &FormatConfig<'_>) {
let mut result = String::new();
if !no_comments {
if !config.no_comments {
let input = leading.trim();
if !input.is_empty() {
for line in input.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
writeln!(result, "{:indent$}{}", "", trimmed, indent = indent).unwrap();
for _ in 0..config.indent_level {
result.push_str(config.indent);
}
writeln!(result, "{}", trimmed).unwrap();
}
}
}
}
write!(result, "{:indent$}", "", indent = indent).unwrap();
for _ in 0..config.indent_level {
result.push_str(config.indent);
}
*leading = result;
}

Expand All @@ -33,3 +145,26 @@ pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
}
*decor = result;
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn builder() -> miette::Result<()> {
let built = FormatConfig::builder()
.indent_level(12)
.indent(" \t")
.no_comments(true)
.build();
assert!(matches!(
built,
FormatConfig {
indent_level: 12,
indent: " \t",
no_comments: true,
}
));
Ok(())
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
pub use document::*;
pub use entry::*;
pub use error::*;
pub use fmt::*;
pub use identifier::*;
pub use node::*;
// pub use query::*;
Expand Down
Loading

0 comments on commit 014c7c5

Please sign in to comment.