feat(css-formatter): implement delimiterSpacing for CSS#9722
Conversation
Add the `delimiterSpacing` formatter option. This option inserts spaces inside delimiters when content fits on a single line. Empty delimiters are not affected, and no space is added before the opening delimiter. The specific delimiters affected depend on the language. Includes the shared DelimiterSpacing type, global configuration, overrides support, Prettier migration, CLI flag, generated schema/bindings, and changeset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughThis PR introduces Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
crates/biome_css_formatter/src/css/auxiliary/document_custom_matcher.rs (1)
22-44: Prefer the shared delimiter helper here instead of hardspace()branches.At the moment this duplicates the
write!block and forces literal spaces whenever enabled. Usingsoft_block_indent_with_maybe_spacekeeps behaviour aligned with the rest of this PR and avoids branch duplication.Suggested simplification
- if should_insert_space { - write!( - f, - [ - name.format(), - l_paren_token.format(), - space(), - value.format(), - space(), - r_paren_token.format() - ] - ) - } else { - write!( - f, - [ - name.format(), - l_paren_token.format(), - value.format(), - r_paren_token.format() - ] - ) - } + write!( + f, + [ + name.format(), + group(&format_args![ + l_paren_token.format(), + soft_block_indent_with_maybe_space(&value.format(), should_insert_space), + r_paren_token.format() + ]) + ] + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_css_formatter/src/css/auxiliary/document_custom_matcher.rs` around lines 22 - 44, Replace the duplicated write! branches that conditionally insert space based on should_insert_space with the shared delimiter helper: call soft_block_indent_with_maybe_space between l_paren_token.format() and value.format() (or wrap the sequence [name.format(), l_paren_token.format(), soft_block_indent_with_maybe_space(...), value.format(), r_paren_token.format()]) so the spacing is handled by the helper and the single write! invocation uses name.format(), l_paren_token.format(), the helper, value.format(), and r_paren_token.format(); remove direct use of space() and the should_insert_space conditional in document_custom_matcher.rs.crates/biome_css_formatter/src/css/auxiliary/keyframes_scope_function.rs (1)
20-44: Consider usingsoft_block_indent_with_maybe_spacefor consistency.This implementation uses explicit
space()calls with if/else branching, while most other formatters in this PR usesoft_block_indent_with_maybe_space. The latter likely handles empty content and multi-line scenarios more gracefully.If there's a specific reason for the different approach here (e.g.,
nameis always a single token that doesn't need soft block indent behaviour), this is fine – just worth noting the divergence.♻️ Alternative using the common pattern
- let should_insert_space = f.options().delimiter_spacing().value(); - - if should_insert_space { - write!( - f, - [ - scope.format(), - l_paren_token.format(), - space(), - name.format(), - space(), - r_paren_token.format(), - ] - ) - } else { - write!( - f, - [ - scope.format(), - l_paren_token.format(), - name.format(), - r_paren_token.format(), - ] - ) - } + let should_insert_space = f.options().delimiter_spacing().value(); + + write!( + f, + [ + scope.format(), + group(&format_args![ + l_paren_token.format(), + soft_block_indent_with_maybe_space(&name.format(), should_insert_space), + r_paren_token.format() + ]) + ] + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_css_formatter/src/css/auxiliary/keyframes_scope_function.rs` around lines 20 - 44, The current if/else uses explicit space() calls around name (checking should_insert_space from f.options().delimiter_spacing()) which diverges from other formatters; replace this branching with the common soft_block_indent_with_maybe_space pattern so scope.format(), l_paren_token.format(), name.format(), and r_paren_token.format() are written using soft_block_indent_with_maybe_space to handle empty/multi-line content consistently; update the write! call that currently references should_insert_space, scope.format(), l_paren_token.format(), name.format(), r_paren_token.format() to use soft_block_indent_with_maybe_space (imported/available in the module) instead of manual space() insertion.crates/biome_service/src/file_handlers/css.rs (1)
237-242: Add a focused precedence test for delimiter spacingThe wiring looks right, but please add a unit test proving precedence (
css.formatter.delimiterSpacingoverrides globalformatter.delimiterSpacing, else fallback to global). It’ll lock this behaviour down and prevent quiet regressions.As per coding guidelines: “All code changes MUST include appropriate tests: … formatters require snapshot tests with valid/invalid cases … and bug fixes require tests that reproduce and validate the fix.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_service/src/file_handlers/css.rs` around lines 237 - 242, Add a unit test that verifies precedence for delimiter spacing by exercising the code path that calls with_delimiter_spacing (using the language.delimiter_spacing.or(global.delimiter_spacing).unwrap_or_default() logic); the test should construct two configs: one where css.formatter.delimiterSpacing is set and a different global.formatter.delimiterSpacing is set (assert the language value wins), and one where css.formatter.delimiterSpacing is absent and global.formatter.delimiterSpacing is used (assert the global value wins), using the same formatter invocation/serialization flow as other css formatter tests/snapshots to lock behaviour.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/biome_configuration/src/css.rs`:
- Around line 104-110: The new Option field `delimiter_spacing:
Option<DelimiterSpacing>` was added but the `default_css` unit test doesn't
assert its default, so add an assertion there that the default value is what you
expect (e.g., `None` or the chosen default variant) to prevent regressions;
locate the `default_css` test and add a single assertion checking
`delimiter_spacing` on the deserialized/default config object (referencing
`delimiter_spacing` and the `DelimiterSpacing` type) to validate the default
state.
In `@crates/biome_css_formatter/src/css/auxiliary/bracketed_value.rs`:
- Around line 15-27: The code currently sets should_insert_space from
f.options().delimiter_spacing().value() and always inserts spaces, causing empty
bracketed values to become `[ ]`; change the guard to also check that the items
collection is non-empty (e.g., compute should_insert_space using
f.options().delimiter_spacing().value() && !items.is_empty()) so that spacing is
only applied when delimiter spacing is enabled and items is not empty; update
the logic around l_brack_token.format(), items.format(), and
r_brack_token.format() in bracketed_value.rs accordingly.
In
`@crates/biome_css_formatter/src/tailwind/auxiliary/custom_variant_shorthand.rs`:
- Around line 22-44: The code unconditionally inserts space() around selector in
the write! blocks—replace those literal space() calls with the existing helper
used elsewhere to apply delimiter spacing only when the content fits on one line
(e.g., the delimiter spacing helper used by other CSS/Tailwind formatters).
Update the two branches that call write!(..., [ l_paren_token.format(), space(),
selector.format(), space(), r_paren_token.format(), semicolon_token.format() ])
and the else branch without spaces to instead call that helper around
selector.format() (keeping l_paren_token.format(), selector.format(),
r_paren_token.format(), semicolon_token.format() intact), and ensure you use
should_insert_space or the helper’s conditional logic rather than a literal
space().
---
Nitpick comments:
In `@crates/biome_css_formatter/src/css/auxiliary/document_custom_matcher.rs`:
- Around line 22-44: Replace the duplicated write! branches that conditionally
insert space based on should_insert_space with the shared delimiter helper: call
soft_block_indent_with_maybe_space between l_paren_token.format() and
value.format() (or wrap the sequence [name.format(), l_paren_token.format(),
soft_block_indent_with_maybe_space(...), value.format(),
r_paren_token.format()]) so the spacing is handled by the helper and the single
write! invocation uses name.format(), l_paren_token.format(), the helper,
value.format(), and r_paren_token.format(); remove direct use of space() and the
should_insert_space conditional in document_custom_matcher.rs.
In `@crates/biome_css_formatter/src/css/auxiliary/keyframes_scope_function.rs`:
- Around line 20-44: The current if/else uses explicit space() calls around name
(checking should_insert_space from f.options().delimiter_spacing()) which
diverges from other formatters; replace this branching with the common
soft_block_indent_with_maybe_space pattern so scope.format(),
l_paren_token.format(), name.format(), and r_paren_token.format() are written
using soft_block_indent_with_maybe_space to handle empty/multi-line content
consistently; update the write! call that currently references
should_insert_space, scope.format(), l_paren_token.format(), name.format(),
r_paren_token.format() to use soft_block_indent_with_maybe_space
(imported/available in the module) instead of manual space() insertion.
In `@crates/biome_service/src/file_handlers/css.rs`:
- Around line 237-242: Add a unit test that verifies precedence for delimiter
spacing by exercising the code path that calls with_delimiter_spacing (using the
language.delimiter_spacing.or(global.delimiter_spacing).unwrap_or_default()
logic); the test should construct two configs: one where
css.formatter.delimiterSpacing is set and a different
global.formatter.delimiterSpacing is set (assert the language value wins), and
one where css.formatter.delimiterSpacing is absent and
global.formatter.delimiterSpacing is used (assert the global value wins), using
the same formatter invocation/serialization flow as other css formatter
tests/snapshots to lock behaviour.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2aa1e169-f6d6-4c2d-84c8-748cd38f7b16
⛔ Files ignored due to path filters (17)
crates/biome_cli/tests/snapshots/main_cases_help/check_help.snapis excluded by!**/*.snapand included by**crates/biome_cli/tests/snapshots/main_cases_help/ci_help.snapis excluded by!**/*.snapand included by**crates/biome_cli/tests/snapshots/main_cases_help/format_help.snapis excluded by!**/*.snapand included by**crates/biome_configuration/tests/invalid/formatter_extraneous_field.json.snapis excluded by!**/*.snapand included by**crates/biome_configuration/tests/invalid/formatter_quote_style.json.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/attribute_selectors.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/boundary_conditions.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/container.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/functions.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/import.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/media_queries.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/pseudo_classes.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/scope.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/supports.css.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/delimiter_spacing/url_functions.css.snapis excluded by!**/*.snapand included by**packages/@biomejs/backend-jsonrpc/src/workspace.tsis excluded by!**/backend-jsonrpc/src/workspace.tsand included by**packages/@biomejs/biome/configuration_schema.jsonis excluded by!**/configuration_schema.jsonand included by**
📒 Files selected for processing (61)
.changeset/delimiter-spacing-css.md.changeset/delimiter-spacing-option.mdcrates/biome_cli/src/execute/migrate/prettier.rscrates/biome_configuration/src/css.rscrates/biome_configuration/src/formatter.rscrates/biome_configuration/src/overrides.rscrates/biome_css_formatter/src/context.rscrates/biome_css_formatter/src/css/auxiliary/attr_function.rscrates/biome_css_formatter/src/css/auxiliary/bracketed_value.rscrates/biome_css_formatter/src/css/auxiliary/container_query_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/container_size_feature_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/container_style_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/container_style_query_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/document_custom_matcher.rscrates/biome_css_formatter/src/css/auxiliary/function.rscrates/biome_css_formatter/src/css/auxiliary/if_function.rscrates/biome_css_formatter/src/css/auxiliary/if_media_test.rscrates/biome_css_formatter/src/css/auxiliary/if_style_test.rscrates/biome_css_formatter/src/css/auxiliary/if_supports_test.rscrates/biome_css_formatter/src/css/auxiliary/if_test_boolean_expr_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/import_named_layer.rscrates/biome_css_formatter/src/css/auxiliary/import_supports.rscrates/biome_css_formatter/src/css/auxiliary/keyframes_scope_function.rscrates/biome_css_formatter/src/css/auxiliary/media_condition_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/media_feature_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/parenthesized_expression.rscrates/biome_css_formatter/src/css/auxiliary/scope_edge.rscrates/biome_css_formatter/src/css/auxiliary/supports_condition_in_parens.rscrates/biome_css_formatter/src/css/auxiliary/supports_feature_declaration.rscrates/biome_css_formatter/src/css/auxiliary/url_function.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_compound_selector_list.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_custom_identifier.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_custom_identifier_list.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_identifier.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_nth.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_relative_selector_list.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_selector_list.rscrates/biome_css_formatter/src/css/pseudo/pseudo_class_function_value_list.rscrates/biome_css_formatter/src/css/pseudo/pseudo_element_function.rscrates/biome_css_formatter/src/css/pseudo/pseudo_element_function_custom_identifier.rscrates/biome_css_formatter/src/css/selectors/attribute_selector.rscrates/biome_css_formatter/src/css/selectors/pseudo_class_function_compound_selector.rscrates/biome_css_formatter/src/css/selectors/pseudo_class_function_selector.rscrates/biome_css_formatter/src/css/selectors/pseudo_element_function_selector.rscrates/biome_css_formatter/src/css/selectors/supports_feature_selector.rscrates/biome_css_formatter/src/tailwind/auxiliary/custom_variant_shorthand.rscrates/biome_css_formatter/src/tailwind/auxiliary/source_inline.rscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/attribute_selectors.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/boundary_conditions.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/container.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/functions.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/import.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/media_queries.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/options.jsoncrates/biome_css_formatter/tests/specs/css/delimiter_spacing/pseudo_classes.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/scope.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/supports.csscrates/biome_css_formatter/tests/specs/css/delimiter_spacing/url_functions.csscrates/biome_formatter/src/lib.rscrates/biome_service/src/file_handlers/css.rscrates/biome_service/src/settings.rs
🦋 Changeset detectedLatest commit: e039333 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will degrade performance by 13.74%
Performance Changes
Comparing Footnotes
|
Summary
Implements
delimiterSpacingfor CSS, building on #9718 (core).For CSS, affects:
rgb( 0, 0, 0 ),:is( .foo ),@supports ( display: grid ),@media ( min-width: 768px ))[ data-attr ])Includes CSS-specific configuration (
css.formatter.delimiterSpacing), service integration, and schema/bindings.AI usage
AI assisted in implementing the CSS formatter changes and writing tests.
Test Plan