diff --git a/crates/biome_css_formatter/src/css/auxiliary/border.rs b/crates/biome_css_formatter/src/css/auxiliary/border.rs index 47bd8b26d03b..f98dd59d8f30 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/border.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/border.rs @@ -1,10 +1,25 @@ -use crate::prelude::*; -use biome_css_syntax::CssBorder; -use biome_rowan::AstNode; +use crate::{prelude::*, utils::properties::FormatPropertyValueFields}; +use biome_css_syntax::{CssBorder, CssBorderFields}; +use biome_formatter::{format_args, write}; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssBorder; impl FormatNodeRule for FormatCssBorder { fn fmt_fields(&self, node: &CssBorder, f: &mut CssFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let CssBorderFields { + line_width, + line_style, + color, + } = node.as_fields(); + + write!( + f, + [FormatPropertyValueFields::new(&format_args![ + line_width.format(), + line_style.format(), + color.format(), + ]) + .with_slot_map(node.concrete_order_slot_map())] + ) } } diff --git a/crates/biome_css_formatter/src/css/auxiliary/line_style.rs b/crates/biome_css_formatter/src/css/auxiliary/line_style.rs index 0858d1cf27af..fa68dee4e8e6 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/line_style.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/line_style.rs @@ -1,10 +1,13 @@ use crate::prelude::*; -use biome_css_syntax::CssLineStyle; -use biome_rowan::AstNode; +use biome_css_syntax::{CssLineStyle, CssLineStyleFields}; +use biome_formatter::write; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssLineStyle; impl FormatNodeRule for FormatCssLineStyle { fn fmt_fields(&self, node: &CssLineStyle, f: &mut CssFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let CssLineStyleFields { keyword } = node.as_fields(); + + write!(f, [keyword.format()]) } } diff --git a/crates/biome_css_formatter/src/css/auxiliary/line_width_keyword.rs b/crates/biome_css_formatter/src/css/auxiliary/line_width_keyword.rs index f1eae653ee34..dffbbc50cc16 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/line_width_keyword.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/line_width_keyword.rs @@ -1,10 +1,13 @@ use crate::prelude::*; -use biome_css_syntax::CssLineWidthKeyword; -use biome_rowan::AstNode; +use biome_css_syntax::{CssLineWidthKeyword, CssLineWidthKeywordFields}; +use biome_formatter::write; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssLineWidthKeyword; impl FormatNodeRule for FormatCssLineWidthKeyword { fn fmt_fields(&self, node: &CssLineWidthKeyword, f: &mut CssFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let CssLineWidthKeywordFields { keyword } = node.as_fields(); + + write!(f, [keyword.format()]) } } diff --git a/crates/biome_css_formatter/src/css/properties/border_property.rs b/crates/biome_css_formatter/src/css/properties/border_property.rs index 6e9a0e1b2795..952e94504b5c 100644 --- a/crates/biome_css_formatter/src/css/properties/border_property.rs +++ b/crates/biome_css_formatter/src/css/properties/border_property.rs @@ -1,10 +1,18 @@ use crate::prelude::*; -use biome_css_syntax::CssBorderProperty; -use biome_rowan::AstNode; +use biome_css_syntax::{CssBorderProperty, CssBorderPropertyFields}; +use biome_formatter::write; #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssBorderProperty; impl FormatNodeRule for FormatCssBorderProperty { fn fmt_fields(&self, node: &CssBorderProperty, f: &mut CssFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let CssBorderPropertyFields { + name, + colon_token, + value, + } = node.as_fields(); + write!( + f, + [name.format(), colon_token.format(), space(), value.format()] + ) } } diff --git a/crates/biome_css_formatter/src/prelude.rs b/crates/biome_css_formatter/src/prelude.rs index 69e4a0665984..9d74d80aa65e 100644 --- a/crates/biome_css_formatter/src/prelude.rs +++ b/crates/biome_css_formatter/src/prelude.rs @@ -7,6 +7,8 @@ pub(crate) use crate::{ }; pub(crate) use biome_formatter::prelude::*; #[allow(unused_imports)] -pub(crate) use biome_rowan::{AstNode as _, AstNodeList as _, AstSeparatedList as _}; +pub(crate) use biome_rowan::{ + AstNode as _, AstNodeList as _, AstNodeSlotMap as _, AstSeparatedList as _, +}; pub(crate) use crate::separated::FormatAstSeparatedListExtension; diff --git a/crates/biome_css_formatter/src/utils/mod.rs b/crates/biome_css_formatter/src/utils/mod.rs index bc7584a88994..159a66203376 100644 --- a/crates/biome_css_formatter/src/utils/mod.rs +++ b/crates/biome_css_formatter/src/utils/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod component_value_list; +pub(crate) mod properties; pub(crate) mod string_utils; diff --git a/crates/biome_css_formatter/src/utils/properties.rs b/crates/biome_css_formatter/src/utils/properties.rs new file mode 100644 index 000000000000..2d6b9d1b97df --- /dev/null +++ b/crates/biome_css_formatter/src/utils/properties.rs @@ -0,0 +1,172 @@ +use crate::prelude::*; +use biome_formatter::{write, Arguments}; + +/// Format all of the fields of a `PropertyValue` node in an arbitrary order, +/// given by `slot_map`. +/// +/// Because the CSS grammar allows rules to specify fields that can appear +/// in any order, there isn't always a linear mapping between the _declared_ +/// order (how they appear in the grammar) and the _concrete_ order (how they +/// appear in the source text) of the fields. The parser supports this by +/// building a `slot_map` to map the declared order to the concrete order. +/// +/// When formatting, by default we want to preserve the ordering of fields as +/// they were written in the source, but just using the `AstNode` alone will +/// naturally re-write the value in the _declared_ order. To preserve the +/// _concrete_ order, we can invert the `slot_map` and sort it to re-determine +/// the ordering of fields and then iterate that list to format each field +/// individually. +/// +/// ## Fields +/// +/// The caller provides a list of _pre-formatted_ fields, using the +/// [`biome_formatter::format_args!`] macro. This way, it can either pass +/// through a field as-is with default formatting, or it can apply any other +/// formatting it once for that field: +/// +/// ```rust,ignore +/// let formatted = format!(CssFormatContext::default(), [ +/// FormatPropertyValueFields::new(&format_args![ +/// text("a"), +/// text("b"), +/// group(&block_indent(&format_args![text("c"), hard_line_break(), text("d")])) +/// ]) +/// .with_slot_map([1, 2, 0]) +/// ])?; +/// +/// assert_eq!("b +/// \tc +/// \td +/// a", formatted.print()?.as_code()); +/// ``` +/// +/// ## Concrete Ordering +/// +/// By default, using this struct will format the fields of the node in order. +/// This is sufficient for nodes that don't have any dynamically-ordered +/// fields, but for dynamic nodes that want to preserve the order of fields as +/// they were given in the input, or for any node that wants to change the +/// ordering of the fields, the caller will need to provide a `slot_map` that +/// this struct can use to re-order the fields. +/// +/// To preserve the field order as it was written in the original source, use +/// [biome_rowan::AstNodeSlotMap::concrete_order_slot_map], which will ensure +/// the ordering matches what was given. This should be the default for most +/// if not all dynamic nodes. +/// +/// ```rust,ignore +/// .with_slot_map(node.concrete_order_slot_map()) +/// ``` +/// +/// Any other method of building a slot map is also valid, but should generally +/// be avoided, as ensuring consistency across formats is difficult without a +/// strong heuristic. +/// +/// ## Grouping Fields (Future) +/// +/// In some cases, a property may want to group certain fields together in +/// order to apply special formatting. As an example, consider a grammar like: +/// +/// ```ebnf +/// font = +/// (style: CssFontStyle || +/// variant: CssFontVariant || +/// weight: CssFontWeight)? +/// size: CssNumber ( '/' line_height: CssLineHeight)? +/// ``` +/// +/// Here, the `style`, `variant`, and `weight` fields can appear conditionally +/// and in any order, but if `line_height` is present, it (and the slash token) +/// must appear immediately adjacent to the `size` field. While it would be +/// valid to just have the fields fill and wrap over lines as needed, the +/// formatter might want to preserve the adjacency and ensure that `size` and +/// `line_height` always get written on the same line. +/// +/// To do this, the value formatter can write both fields in a single group, +/// and then use an `empty_field_slot` value in the slots where the other +/// fields have been taken from: +/// +/// ```rust,ignore +/// FormatPropertyValueFields::new(&format_args![ +/// style.format(), +/// variant.format(), +/// weight.format(), +/// group(&format_args![ +/// size.format(), slash_token.format(), line_height.format() +/// ]), +/// empty_field_slot(), +/// empty_field_slot() +/// ]) +/// .with_slot_map(node.concrete_order_slot_map()) +/// ``` +/// +/// The `empty_field_slot()` values will tell this struct to skip formatting +/// for that field, with the assumption that another field includes its value. +pub struct FormatPropertyValueFields<'fmt, const N: usize> { + slot_map: Option<[u8; N]>, + fields: &'fmt Arguments<'fmt, CssFormatContext>, +} + +impl<'fmt, const N: usize> FormatPropertyValueFields<'fmt, N> { + pub fn new(fields: &'fmt Arguments<'fmt, CssFormatContext>) -> Self { + Self { + slot_map: None, + fields, + } + } + + pub fn with_slot_map(mut self, slot_map: [u8; N]) -> Self { + debug_assert!( + self.fields.items().len() == N, + "slot_map must specify the same number of fields as this struct contains" + ); + self.slot_map = Some(slot_map); + self + } +} + +impl<'fmt, const N: usize> Format for FormatPropertyValueFields<'fmt, N> { + fn fmt(&self, f: &mut CssFormatter) -> FormatResult<()> { + let values = format_with(|f: &mut Formatter<'_, CssFormatContext>| { + let mut filler = f.fill(); + + // First, determine the ordering of fields to use. If no slot_map is + // provided along with the fields, then they can just be used in the + // same order, but if a `slot_map` is present, then the fields are + // re-ordered to match the concrete ordering from the source syntax. + // + // The fields are wrapped with `Option` for two reasons: for nodes + // with slot maps, it simplifies how the re-ordered slice is built, and + // it also allows empty/missing fields to be removed in the next step. + match self.slot_map { + None => { + for field in self.fields.items() { + filler.entry(&soft_line_break_or_space(), field); + } + } + Some(slot_map) => { + for slot in slot_map { + // This condition ensures that missing values are _not_ included in the + // fill. The generated `slot_map` for an AstNode guarantees that all + // present fields have a tangible value here, while all absent fields + // have this sentinel value ([biome_css_syntax::SLOT_MAP_EMPTY_VALUE]). + // + // This check is important to ensure that we don't add empty values to + // the fill, since that would add double separators when we don't want + // them. + if slot == u8::MAX { + continue; + } + + let field = &self.fields.items()[slot as usize]; + filler.entry(&soft_line_break_or_space(), field); + } + } + }; + + filler.finish() + }); + + write!(f, [group(&indent(&values))]) + } +} diff --git a/crates/biome_css_formatter/tests/quick_test.rs b/crates/biome_css_formatter/tests/quick_test.rs index 81de0a5b3248..079a9b62b44c 100644 --- a/crates/biome_css_formatter/tests/quick_test.rs +++ b/crates/biome_css_formatter/tests/quick_test.rs @@ -14,9 +14,11 @@ mod language { fn quick_test() { let src = r#" div { - prod: fn(100px); - prod: --fn(100px); - prod: --fn--fn(100px); + border: #fff solid + + 2px; + border: THICK #000; + border: medium; } "#; diff --git a/crates/biome_css_formatter/tests/specs/css/properties/border.css b/crates/biome_css_formatter/tests/specs/css/properties/border.css new file mode 100644 index 000000000000..5d842f5d1c86 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/properties/border.css @@ -0,0 +1,39 @@ +div { + /* Generic property tests */ + border: InItial; + border + : + inherit + ; + + border : zzz-unknown-value ; + border : a, + value list ; + + + /* */ + border : SOLID; + border: none + ; + + /* */ + border : ThIn; + border: + medium + ; + border: 100px; + + /* */ + border: + #fff; + + /* combinations */ + border: 2px + dotted; + border : outset #f33; + border:#000 medium + + dashed + + ; +} diff --git a/crates/biome_css_formatter/tests/specs/css/properties/border.css.snap b/crates/biome_css_formatter/tests/specs/css/properties/border.css.snap new file mode 100644 index 000000000000..c17a650c7980 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/properties/border.css.snap @@ -0,0 +1,94 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/properties/border.css +--- + +# Input + +```css +div { + /* Generic property tests */ + border: InItial; + border + : + inherit + ; + + border : zzz-unknown-value ; + border : a, + value list ; + + + /* */ + border : SOLID; + border: none + ; + + /* */ + border : ThIn; + border: + medium + ; + border: 100px; + + /* */ + border: + #fff; + + /* combinations */ + border: 2px + dotted; + border : outset #f33; + border:#000 medium + + dashed + + ; +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +----- + +```css +div { + /* Generic property tests */ + border: initial; + border: inherit; + + border: zzz-unknown-value; + border: a , value list; + + /* */ + border: solid; + border: none; + + /* */ + border: thin; + border: medium; + border: 100px; + + /* */ + border: #fff; + + /* combinations */ + border: 2px dotted; + border: outset #f33; + border: #000 medium dashed; +} +``` + + diff --git a/crates/biome_css_syntax/src/generated/nodes.rs b/crates/biome_css_syntax/src/generated/nodes.rs index a8c8c6eb444c..190684f6e3ce 100644 --- a/crates/biome_css_syntax/src/generated/nodes.rs +++ b/crates/biome_css_syntax/src/generated/nodes.rs @@ -12,7 +12,8 @@ use crate::{ use biome_rowan::{support, AstNode, RawSyntaxKind, SyntaxKindSet, SyntaxResult}; #[allow(unused)] use biome_rowan::{ - AstNodeList, AstNodeListIterator, AstSeparatedList, AstSeparatedListNodesIterator, + AstNodeList, AstNodeListIterator, AstNodeSlotMap, AstSeparatedList, + AstSeparatedListNodesIterator, }; #[cfg(feature = "serde")] use serde::ser::SerializeSeq; @@ -7929,6 +7930,11 @@ impl AstNode for CssBorder { self.syntax } } +impl AstNodeSlotMap<3usize> for CssBorder { + fn slot_map(&self) -> &[u8; 3usize] { + &self.slot_map + } +} impl std::fmt::Debug for CssBorder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CssBorder") diff --git a/crates/biome_formatter/src/arguments.rs b/crates/biome_formatter/src/arguments.rs index ff96b33e0d89..4e40e10fcc74 100644 --- a/crates/biome_formatter/src/arguments.rs +++ b/crates/biome_formatter/src/arguments.rs @@ -56,6 +56,13 @@ impl<'fmt, Context> Argument<'fmt, Context> { } } +impl<'fmt, Context> Format for Argument<'fmt, Context> { + #[inline(always)] + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + self.format(f) + } +} + /// Sequence of objects that should be formatted in the specified order. /// /// The [`format_args!`] macro will safely create an instance of this structure. @@ -87,7 +94,7 @@ impl<'fmt, Context> Arguments<'fmt, Context> { /// Returns the arguments #[inline] - pub(super) fn items(&self) -> &'fmt [Argument<'fmt, Context>] { + pub fn items(&self) -> &'fmt [Argument<'fmt, Context>] { self.0 } } diff --git a/crates/biome_js_syntax/src/generated/nodes.rs b/crates/biome_js_syntax/src/generated/nodes.rs index dfa42cc07991..15cbdccbfd41 100644 --- a/crates/biome_js_syntax/src/generated/nodes.rs +++ b/crates/biome_js_syntax/src/generated/nodes.rs @@ -12,7 +12,8 @@ use crate::{ use biome_rowan::{support, AstNode, RawSyntaxKind, SyntaxKindSet, SyntaxResult}; #[allow(unused)] use biome_rowan::{ - AstNodeList, AstNodeListIterator, AstSeparatedList, AstSeparatedListNodesIterator, + AstNodeList, AstNodeListIterator, AstNodeSlotMap, AstSeparatedList, + AstSeparatedListNodesIterator, }; #[cfg(feature = "serde")] use serde::ser::SerializeSeq; diff --git a/crates/biome_json_syntax/src/generated/nodes.rs b/crates/biome_json_syntax/src/generated/nodes.rs index b755da777f4c..6da549e9f223 100644 --- a/crates/biome_json_syntax/src/generated/nodes.rs +++ b/crates/biome_json_syntax/src/generated/nodes.rs @@ -12,7 +12,8 @@ use crate::{ use biome_rowan::{support, AstNode, RawSyntaxKind, SyntaxKindSet, SyntaxResult}; #[allow(unused)] use biome_rowan::{ - AstNodeList, AstNodeListIterator, AstSeparatedList, AstSeparatedListNodesIterator, + AstNodeList, AstNodeListIterator, AstNodeSlotMap, AstSeparatedList, + AstSeparatedListNodesIterator, }; #[cfg(feature = "serde")] use serde::ser::SerializeSeq; diff --git a/crates/biome_rowan/src/ast/mod.rs b/crates/biome_rowan/src/ast/mod.rs index 4556537a4b22..0462d6dd642c 100644 --- a/crates/biome_rowan/src/ast/mod.rs +++ b/crates/biome_rowan/src/ast/mod.rs @@ -319,6 +319,83 @@ pub trait AstNode: Clone { } } +/// An AstNode that supports dynamic ordering of the fields it contains uses a +/// `slot_map` to map the _declared_ order of fields to the _concrete_ order +/// as parsed from the source content. Implementing this trait lets consumers +/// +pub trait AstNodeSlotMap { + /// Return the internal slot_map that was built when constructed from the + /// underlying [SyntaxNode]. + fn slot_map(&self) -> &[u8; N]; + + /// Invert and sort the `slot_map` for the [AstNode] to return a mapping of + /// _concrete_ field ordering from the source to the _declared_ ordering of + /// the [AstNode]. + /// + /// Note that the _entire_ slot map is inverted and returned, including + /// both the ordered and the unordered fields. Ordered fields will have + /// their slot positions fixed in both the original and the inverted slot + /// maps, since they can't be moved. Ordered fields also act as boundary + /// points for unordered fields, meaning the concrete order will never + /// allow the concrete slot of an unordered field to appear on the opposite + /// side of an ordered field, even if the field is empty, and the ordered + /// fields will _always_ have the same slot in both maps. + /// + /// Example: Given a grammar like: + /// MultiplyVectorsNode = + /// (Color + /// || Number + /// || String) + /// 'x' + /// (Color + /// || Number + /// || String) + /// There are two sets of unordered fields here (the groups combined with + /// `||` operators). Each contains three fields, and then there is a single + /// ordered field between them, the `x` token. This Node declares a + /// `slot_map` with 7 indices. The first three can be mapped in any order, + /// and the last three can be mapped in any order, but the `x` token will + /// _always_ occupy the fourth slot (zero-based index 3). + /// + /// Now, given an input like `10 "hello" #fff x "bye" #000 20`, the + /// constructed [AstNode]'s slot_map would look like + /// `[2, 0, 1, 3, 6, 4, 5]`. The first `Color` field, declared as index 0, + /// appears at the 2nd index in the concrete source, so the value at index + /// 0 is 2, and so on for the rest of the fields. + /// + /// The inversion of this slot map, then, is `[1, 2, 0, 3, 5, 6, 4]`. To + /// compare these, think: the value 0 in the original `slot_map` appeared + /// at index 1, so index 0 in the inverted map has the _value_ 1, then + /// apply that for of the slots. As you can see `3` is still in the same + /// position, because it is an ordered field. + /// + /// ## Optional Fields + /// + /// It's also possible for unordered fields to be _optional_, meaning they + /// are not present in the concrete source. In this case, the sentinel + /// value of `255` ([`std::u8::MAX`]) is placed in the slot map. When + /// inverting the map, if a slot index cannot be found in the map, it is + /// preserved as the same sentinel value in the inverted map. + /// + /// Using the same grammar as before, the input `10 x #000` is also valid, + /// but is missing many of the optional fields. The `slot_map` for this + /// node would include sentinel values for all of the missing fields, like: + /// `[255, 0, 255, 3, 4, 255, 255]`. Inverting this map would then yield: + /// `[1, 255, 255, 3, 4, 255, 255]`. Each declared slot is still + /// represented in the inverted map, but only the fields that exist in the + /// concrete source have usable values. + fn concrete_order_slot_map(&self) -> [u8; N] { + let mut inverted = [u8::MAX; N]; + for (declared_slot, concrete_slot) in self.slot_map().iter().enumerate() { + if *concrete_slot != u8::MAX { + inverted[*concrete_slot as usize] = declared_slot as u8; + } + } + + inverted + } +} + pub trait SyntaxNodeCast { /// Tries to cast the current syntax node to specified AST node. /// diff --git a/xtask/codegen/src/generate_nodes.rs b/xtask/codegen/src/generate_nodes.rs index 0b12e5c2bf9b..5a22ed522792 100644 --- a/xtask/codegen/src/generate_nodes.rs +++ b/xtask/codegen/src/generate_nodes.rs @@ -223,6 +223,18 @@ pub fn generate_nodes(ast: &AstSrc, language_kind: LanguageKind) -> Result for #name { + fn slot_map(&self) -> &#slot_map_type { + &self.slot_map + } + } + } + } else { + Default::default() + }; + ( quote! { // TODO: review documentation @@ -277,6 +289,8 @@ pub fn generate_nodes(ast: &AstSrc, language_kind: LanguageKind) -> Result SyntaxNode { self.syntax } } + #ast_node_slot_map_impl + impl std::fmt::Debug for #name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(#string_name) @@ -883,9 +897,9 @@ pub fn generate_nodes(ast: &AstSrc, language_kind: LanguageKind) -> Result