Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::prelude::*;
use crate::utils::comment_trivia::has_inline_trailing_comment;
use crate::utils::scss_include_keyword_value::is_top_level_include_keyword_parenthesized_value;
use crate::utils::scss_list_layout::has_scss_list_shape;
use crate::utils::scss_separator_comments::FormatScssSeparatorComments;
use biome_css_syntax::{
AnyScssExpression, AnyScssExpressionItem, ScssParenthesizedExpression,
ScssParenthesizedExpressionFields, is_scss_map_outer_parenthesized_value,
is_scss_parenthesized_expression, scss_include_keyword_argument_owner,
unwrap_single_expression_item,
AnyScssExpression, ScssParenthesizedExpression, ScssParenthesizedExpressionFields,
is_scss_map_outer_parenthesized_value, is_scss_parenthesized_expression,
scss_include_keyword_argument_owner, unwrap_single_expression_item,
};
use biome_formatter::{format_args, write};

Expand Down Expand Up @@ -50,9 +50,9 @@ impl<'a> ScssParenthesizedExpressionLayout<'a> {
Self { node }
}

/// Formats the trailing comma inside parentheses that Prettier breaks.
/// Formats the trailing comma inside broken nested map parentheses.
///
/// Examples: `@include mix($arg: (a))`, `key: (value)`.
/// Examples: `@include mix($arg: ((a: b)))`, `key: ((a: b))`.
fn write_trailing_comma(&self, f: &mut CssFormatter) -> FormatResult<()> {
if self.should_print_trailing_comma() {
write!(f, [if_group_breaks(&token(","))])
Expand All @@ -61,45 +61,66 @@ impl<'a> ScssParenthesizedExpressionLayout<'a> {
}
}

/// Expands parenthesized map values and include keyword values.
/// Expands parenthesized map values and include keyword lists.
///
/// Examples: `key: (value)`, `@include mix($arg: (a))`.
/// `key: (value)` and `2 * ($bar)` stay inline; `key: (a, b)` expands.
fn should_expand(&self) -> bool {
is_scss_map_outer_parenthesized_value(self.node)
|| is_top_level_include_keyword_parenthesized_value(self.node)
self.should_expand_map_value()
|| self.should_expand_include_value()
|| self.has_nested_include_parentheses()
|| self.has_include_trailing_comment()
}

/// Allows a trailing comma for parentheses Prettier treats as one-item lists.
/// Allows a trailing comma for parentheses Prettier treats as lists.
///
/// Examples: `@include mix($arg: (a))`, `key: (value)`.
/// Examples: `@include mix($arg: ((a)))`, `key: (a: b)`.
fn should_print_trailing_comma(&self) -> bool {
self.should_print_map_trailing_comma()
|| self.has_nested_include_parentheses()
|| self.should_print_include_trailing_comma()
}

/// Allows the trailing comma in maps such as `key: (value)`.
/// Expands map values with list or map payloads.
///
/// `key: (value)` stays inline, but `key: (a, b)` and `key: (a: b)` expand.
fn should_expand_map_value(&self) -> bool {
is_scss_map_outer_parenthesized_value(self.node) && self.has_list_or_map_payload()
}

/// Expands include keyword values with list or map payloads.
///
/// `2 * ($bar)` stays inline, but `$arg: ($bar, $baz)` expands.
fn should_expand_include_value(&self) -> bool {
is_top_level_include_keyword_parenthesized_value(self.node)
&& self.has_list_or_map_payload()
}

fn has_list_or_map_payload(&self) -> bool {
self.node.expression().ok().is_some_and(|expression| {
expression_has_list_shape(&expression) || expression_is_map(&expression)
})
}

/// Allows the trailing comma around nested maps such as `key: ((a: b))`.
fn should_print_map_trailing_comma(&self) -> bool {
is_scss_map_outer_parenthesized_value(self.node)
&& self
.node
.expression()
.ok()
.is_some_and(|expression| !expression_owns_list_comma(&expression))
.is_some_and(|expression| expression_is_map(&expression))
}

/// Allows the top-level include comma unless the child list owns commas.
/// Allows the top-level include comma around nested maps.
///
/// Example: `$arg: (a)` prints a comma, but `$arg: (a, b)` does not.
/// Example: `$arg: ((a: b))`.
fn should_print_include_trailing_comma(&self) -> bool {
is_top_level_include_keyword_parenthesized_value(self.node)
&& self
.node
.expression()
.ok()
.is_some_and(|expression| !expression_owns_list_comma(&expression))
.is_some_and(|expression| expression_is_map(&expression))
}

/// Detects the outer include value in `@include mix($arg: ((a)))`.
Expand Down Expand Up @@ -144,12 +165,20 @@ impl Format<CssFormatContext> for ScssParenthesizedExpressionLayout<'_> {
}
}

/// Lists already print their own item comma in `$arg: (a, b)`.
///
/// Maps only print pair separators, so `key: ((a: b))` still needs the outer
/// scalar comma after the nested map.
fn expression_owns_list_comma(expression: &AnyScssExpression) -> bool {
matches!(expression, AnyScssExpression::ScssListExpression(_))
/// Returns `true` for list payloads with separators, such as `$arg: (a, b)`.
fn expression_has_list_shape(expression: &AnyScssExpression) -> bool {
expression
.as_scss_list_expression()
.is_some_and(has_scss_list_shape)
|| unwrap_single_expression_item(expression).is_some_and(|item| {
item.as_scss_list_expression()
.is_some_and(has_scss_list_shape)
})
}

/// Returns `true` for map payloads, such as `key: (a: b)`.
fn expression_is_map(expression: &AnyScssExpression) -> bool {
expression.as_scss_map_expression().is_some()
|| unwrap_single_expression_item(expression)
.is_some_and(|item| matches!(item, AnyScssExpressionItem::ScssListExpression(_)))
.is_some_and(|item| item.as_scss_map_expression().is_some())
}
97 changes: 52 additions & 45 deletions crates/biome_css_formatter/src/utils/scss_list_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl<'a> ScssListLayout<'a> {
);
}

if is_scss_map_outer_parenthesized_value_list(self.node) {
if is_scss_map_outer_parenthesized_value_list(self.node) && has_list_shape(&elements) {
// `key: (a, b)` gets its block indent from the parentheses;
// the list only forces item breaks and the trailing comma.
return write!(
Expand Down Expand Up @@ -103,47 +103,39 @@ impl<'a> ScssListLayout<'a> {
f: &mut CssFormatter,
) -> FormatResult<()> {
let should_force_trailing_comma = self.should_force_include_trailing_comma(elements, f);
let trailing_comma =
format_with(|f| Self::write_include_trailing_comma(should_force_trailing_comma, f));
let is_scalar_include_parentheses = self.is_scalar_include_parentheses(elements);
let trailing_comma = format_with(|f| {
if should_force_trailing_comma {
write!(f, [token(",")])
} else if is_scalar_include_parentheses {
// Do not turn `2 * ($bar)` into `2 * ($bar,)`.
Ok(())
} else {
write!(f, [if_group_breaks(&token(","))])
}
});
let closing_comments = format_with(|f| self.write_include_closing_comments(f));
let should_expand = self.should_expand_include_list(elements, f);

if is_scss_map_outer_parenthesized_value_list(self.node)
|| self.is_parenthesized_include_list()
{
// Format the list in `key: (a, b)`. The surrounding parentheses are
// handled by `FormatScssParenthesizedExpression`.
write!(
f,
[group(&format_args![
elements.format(),
trailing_comma, closing_comments
])
.should_expand(should_expand)]
)
} else {
write!(
f,
[group(&indent(&format_args![
soft_line_break(),
elements.format(),
trailing_comma,
closing_comments
]))
.should_expand(should_expand)]
)
}
}

/// Formats the comma after an include argument list.
///
/// Example: `@include mix($arg: (a))` prints the keyword list comma.
fn write_include_trailing_comma(should_force: bool, f: &mut CssFormatter) -> FormatResult<()> {
if should_force {
write!(f, [token(",")])
} else {
write!(f, [if_group_breaks(&token(","))])
}
let parenthesized_list = is_scss_map_outer_parenthesized_value_list(self.node)
|| self.is_parenthesized_include_list();
let content = format_once(|f| {
if parenthesized_list {
// `key: (a, b)` gets its indent from the surrounding parentheses.
write!(f, [elements.format(), trailing_comma, closing_comments])
} else {
write!(
f,
[indent(&format_args![
soft_line_break(),
elements.format(),
trailing_comma,
closing_comments
])]
)
}
});

write!(f, [group(&content).should_expand(should_expand)])
}

/// Formats include-owned comments before the closing `)`.
Expand All @@ -155,7 +147,7 @@ impl<'a> ScssListLayout<'a> {

/// Forces a comma when include parens or comments own the list shape.
///
/// Examples: `@include mix($arg: (a))`, `@include mix((a) /* end */)`.
/// Examples: `@include mix($arg: (a, b))`, `@include mix((a) /* end */)`.
fn should_force_include_trailing_comma(
&self,
elements: &ScssListExpressionElementList,
Expand All @@ -167,7 +159,7 @@ impl<'a> ScssListLayout<'a> {

/// Expands include keyword lists that need visible item/comment boundaries.
///
/// Examples: `@include mix($arg: (a))`, `@include mix($arg: (a) /* end */)`.
/// Examples: `@include mix($arg: (a, b))`, `@include mix($arg: (a) /* end */)`.
fn should_expand_include_list(
&self,
elements: &ScssListExpressionElementList,
Expand All @@ -179,13 +171,17 @@ impl<'a> ScssListLayout<'a> {

/// Forces expansion and a comma for include keyword lists in parens.
///
/// Examples: `$arg: (a)`, `$arg: (a,)`.
/// Examples: `$arg: (a, b)`, `$arg: (a,)`.
fn should_force_include_parenthesized_list_layout(
&self,
elements: &ScssListExpressionElementList,
) -> bool {
self.is_parenthesized_include_list()
&& (elements.len() > 0 || elements.trailing_separator().is_some())
self.is_parenthesized_include_list() && has_list_shape(elements)
}

/// Detects scalar include parentheses such as `2 * ($bar)`.
fn is_scalar_include_parentheses(&self, elements: &ScssListExpressionElementList) -> bool {
self.is_parenthesized_include_list() && !has_list_shape(elements)
}

/// Detects the list in `@include mix($arg: (a, b))`.
Expand Down Expand Up @@ -270,6 +266,17 @@ fn has_compound_list_element(elements: &ScssListExpressionElementList) -> bool {
.any(|element| element.as_ref().is_ok_and(is_compound_list_element))
}

/// Returns `true` for SCSS lists with visible separators.
///
/// `(a, b)` and `(a,)` are lists; `(a)` is scalar parentheses.
pub(crate) fn has_scss_list_shape(node: &ScssListExpression) -> bool {
has_list_shape(&node.elements())
}

fn has_list_shape(elements: &ScssListExpressionElementList) -> bool {
elements.len() > 1 || elements.trailing_separator().is_some()
}

/// Checks whether `"save" 50px` has multiple values.
fn is_compound_list_element(element: &ScssListExpressionElement) -> bool {
element.value().ok().is_some_and(|value| {
Expand Down

This file was deleted.

Loading
Loading