feat(fmt): Configurable autoformat with `FormatConfig` (#95)

Fixes: https://github.com/kdl-org/kdl-rs/issues/85
This commit is contained in:
Tamme Schichler 2024-12-06 20:59:20 +00:00 committed by GitHub
parent 876a4276bd
commit 014c7c57a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 283 additions and 76 deletions

View File

@ -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() {

View File

@ -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;
}
@ -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(())
}
}

View File

@ -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::*;

View File

@ -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<'_>,