From 014c7c57a2089dae78cd9136ab9bbdf8aa3f049f Mon Sep 17 00:00:00 2001 From: Tamme Schichler Date: Fri, 6 Dec 2024 20:59:20 +0000 Subject: [PATCH] feat(fmt): Configurable autoformat with `FormatConfig` (#95) Fixes: https://github.com/kdl-org/kdl-rs/issues/85 --- src/document.rs | 103 +++++++++++++++++++++++++++------- src/fmt.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 1 + src/node.rs | 112 +++++++++++++++++++------------------ 4 files changed, 283 insertions(+), 76 deletions(-) diff --git a/src/document.rs b/src/document.rs index f9ea66f..f2df91d 100644 --- a/src/document.rs +++ b/src/document.rs @@ -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). @@ -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, @@ -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<'_>, @@ -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() { diff --git a/src/fmt.rs b/src/fmt.rs index 62bfbef..fbc927c 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -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) -> 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) -> 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; } @@ -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(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index e9affe2..1c3ca5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; diff --git a/src/node.rs b/src/node.rs index 1ea9532..b7eb33a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -8,11 +8,10 @@ use std::{ use miette::SourceSpan; use crate::{ - v2_parser, KdlDocument, KdlDocumentFormat, KdlEntry, KdlIdentifier, KdlParseFailure, KdlValue, + v2_parser, FormatConfig, KdlDocument, KdlDocumentFormat, KdlEntry, KdlIdentifier, + KdlParseFailure, KdlValue, }; -static INDENT: usize = 4; - /// Represents an individual KDL /// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a /// KDL Document. @@ -388,12 +387,68 @@ impl KdlNode { } /// Auto-formats this node and its contents. pub fn autoformat(&mut self) { - self.autoformat_impl(0, false); + self.autoformat_config(&FormatConfig::default()); } /// Auto-formats this node and its contents, stripping comments. pub fn autoformat_no_comments(&mut self) { - self.autoformat_impl(0, true); + self.autoformat_config(&FormatConfig { + no_comments: true, + ..Default::default() + }); + } + + /// Auto-formats this node and its contents according to `config`. + pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) { + if let Some(KdlNodeFormat { + leading, + before_terminator, + terminator, + trailing, + before_children, + .. + }) = self.format_mut() + { + crate::fmt::autoformat_leading(leading, config); + crate::fmt::autoformat_trailing(before_terminator, config.no_comments); + crate::fmt::autoformat_trailing(trailing, config.no_comments); + *trailing = trailing.trim().into(); + if !terminator.starts_with('\n') { + *terminator = "\n".into(); + } + if let Some(c) = trailing.chars().next() { + if !c.is_whitespace() { + trailing.insert(0, ' '); + } + } + + *before_children = " ".into(); + } else { + self.set_format(KdlNodeFormat { + terminator: "\n".into(), + ..Default::default() + }) + } + self.name.clear_format(); + if let Some(ty) = self.ty.as_mut() { + ty.clear_format() + } + for entry in &mut self.entries { + entry.autoformat(); + } + if let Some(children) = self.children.as_mut() { + children.autoformat_config(&FormatConfig { + indent_level: config.indent_level + 1, + ..*config + }); + if let Some(KdlDocumentFormat { leading, trailing }) = children.format_mut() { + *leading = leading.trim().into(); + leading.push('\n'); + for _ in 0..config.indent_level { + trailing.push_str(config.indent); + } + } + } } // TODO(@zkat): These should all be moved into the query module, instead @@ -512,53 +567,6 @@ impl Display for KdlNode { } impl KdlNode { - pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) { - if let Some(KdlNodeFormat { - leading, - before_terminator, - terminator, - trailing, - before_children, - .. - }) = self.format_mut() - { - crate::fmt::autoformat_leading(leading, indent, no_comments); - crate::fmt::autoformat_trailing(before_terminator, no_comments); - crate::fmt::autoformat_trailing(trailing, no_comments); - *trailing = trailing.trim().into(); - if !terminator.starts_with('\n') { - *terminator = "\n".into(); - } - if let Some(c) = trailing.chars().next() { - if !c.is_whitespace() { - trailing.insert(0, ' '); - } - } - - *before_children = " ".into(); - } else { - self.set_format(KdlNodeFormat { - terminator: "\n".into(), - ..Default::default() - }) - } - self.name.clear_format(); - if let Some(ty) = self.ty.as_mut() { - ty.clear_format() - } - for entry in &mut self.entries { - entry.autoformat(); - } - if let Some(children) = self.children.as_mut() { - children.autoformat_impl(indent + INDENT, no_comments); - if let Some(KdlDocumentFormat { leading, trailing }) = children.format_mut() { - *leading = leading.trim().into(); - leading.push('\n'); - trailing.push_str(format!("{:indent$}", "", indent = indent).as_str()); - } - } - } - pub(crate) fn stringify( &self, f: &mut std::fmt::Formatter<'_>,