Skip to content

feat(css-formatter): implement delimiterSpacing for CSS#9722

Merged
ematipico merged 7 commits into
biomejs:nextfrom
luisherranz:feat/delimiter-spacing-css
Apr 24, 2026
Merged

feat(css-formatter): implement delimiterSpacing for CSS#9722
ematipico merged 7 commits into
biomejs:nextfrom
luisherranz:feat/delimiter-spacing-css

Conversation

@luisherranz

@luisherranz luisherranz commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements delimiterSpacing for CSS, building on #9718 (core).

For CSS, affects:

  • Parentheses (e.g., rgb( 0, 0, 0 ), :is( .foo ), @supports ( display: grid ), @media ( min-width: 768px ))
  • Square brackets (e.g., [ data-attr ])
- a[href] { color: rgba(0, 0, 0, 1); }
+ a[ href ] { color: rgba( 0, 0, 0, 1 ); }

Includes CSS-specific configuration (css.formatter.delimiterSpacing), service integration, and schema/bindings.

Depends on: #9718 (core). Since this PR is from a fork, the base is set to next. The diff will include core commits until core is merged.

AI usage

AI assisted in implementing the CSS formatter changes and writing tests.

Test Plan

  • Added snapshot tests for attribute selectors, functions, container queries, media queries, pseudo classes, scope, supports, URL functions, imports, and boundary conditions

luisherranz and others added 2 commits March 30, 2026 11:43
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>
@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools A-LSP Area: language server protocol L-JavaScript Language: JavaScript and super languages L-CSS Language: CSS and super languages L-JSON Language: JSON and super languages A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages L-Grit Language: GritQL A-Resolver Area: resolver labels Mar 30, 2026
@luisherranz luisherranz changed the base branch from main to next March 30, 2026 14:26
@luisherranz luisherranz marked this pull request as ready for review April 1, 2026 09:09
@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces DelimiterSpacing, a new formatter configuration option for CSS that controls spacing inside parentheses and square brackets. When enabled (via formatter.delimiterSpacing or css.formatter.delimiterSpacing), the formatter adds spaces after opening and before closing delimiters when the contained content fits on a single line. Empty delimiters remain unchanged. The change spans configuration infrastructure, CSS formatter implementations for 30+ constructs (functions, pseudo-classes, attribute selectors, etc.), type definitions, and comprehensive test fixtures covering various CSS syntaxes.

Possibly related PRs

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: implementing delimiterSpacing support for the CSS formatter.
Description check ✅ Passed The description clearly relates to the changeset, explaining what delimiterSpacing does, which constructs it affects, configuration options, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hard space() branches.

At the moment this duplicates the write! block and forces literal spaces whenever enabled. Using soft_block_indent_with_maybe_space keeps 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 using soft_block_indent_with_maybe_space for consistency.

This implementation uses explicit space() calls with if/else branching, while most other formatters in this PR use soft_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., name is 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 spacing

The wiring looks right, but please add a unit test proving precedence (css.formatter.delimiterSpacing overrides global formatter.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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee64f8 and d1c2c2c.

⛔ Files ignored due to path filters (17)
  • crates/biome_cli/tests/snapshots/main_cases_help/check_help.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_help/ci_help.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_help/format_help.snap is excluded by !**/*.snap and included by **
  • crates/biome_configuration/tests/invalid/formatter_extraneous_field.json.snap is excluded by !**/*.snap and included by **
  • crates/biome_configuration/tests/invalid/formatter_quote_style.json.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/attribute_selectors.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/boundary_conditions.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/container.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/functions.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/import.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/media_queries.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/pseudo_classes.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/scope.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/supports.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/url_functions.css.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (61)
  • .changeset/delimiter-spacing-css.md
  • .changeset/delimiter-spacing-option.md
  • crates/biome_cli/src/execute/migrate/prettier.rs
  • crates/biome_configuration/src/css.rs
  • crates/biome_configuration/src/formatter.rs
  • crates/biome_configuration/src/overrides.rs
  • crates/biome_css_formatter/src/context.rs
  • crates/biome_css_formatter/src/css/auxiliary/attr_function.rs
  • crates/biome_css_formatter/src/css/auxiliary/bracketed_value.rs
  • crates/biome_css_formatter/src/css/auxiliary/container_query_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/container_size_feature_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/container_style_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/container_style_query_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/document_custom_matcher.rs
  • crates/biome_css_formatter/src/css/auxiliary/function.rs
  • crates/biome_css_formatter/src/css/auxiliary/if_function.rs
  • crates/biome_css_formatter/src/css/auxiliary/if_media_test.rs
  • crates/biome_css_formatter/src/css/auxiliary/if_style_test.rs
  • crates/biome_css_formatter/src/css/auxiliary/if_supports_test.rs
  • crates/biome_css_formatter/src/css/auxiliary/if_test_boolean_expr_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/import_named_layer.rs
  • crates/biome_css_formatter/src/css/auxiliary/import_supports.rs
  • crates/biome_css_formatter/src/css/auxiliary/keyframes_scope_function.rs
  • crates/biome_css_formatter/src/css/auxiliary/media_condition_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/media_feature_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/parenthesized_expression.rs
  • crates/biome_css_formatter/src/css/auxiliary/scope_edge.rs
  • crates/biome_css_formatter/src/css/auxiliary/supports_condition_in_parens.rs
  • crates/biome_css_formatter/src/css/auxiliary/supports_feature_declaration.rs
  • crates/biome_css_formatter/src/css/auxiliary/url_function.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_compound_selector_list.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_custom_identifier.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_custom_identifier_list.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_identifier.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_nth.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_relative_selector_list.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_selector_list.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_class_function_value_list.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_element_function.rs
  • crates/biome_css_formatter/src/css/pseudo/pseudo_element_function_custom_identifier.rs
  • crates/biome_css_formatter/src/css/selectors/attribute_selector.rs
  • crates/biome_css_formatter/src/css/selectors/pseudo_class_function_compound_selector.rs
  • crates/biome_css_formatter/src/css/selectors/pseudo_class_function_selector.rs
  • crates/biome_css_formatter/src/css/selectors/pseudo_element_function_selector.rs
  • crates/biome_css_formatter/src/css/selectors/supports_feature_selector.rs
  • crates/biome_css_formatter/src/tailwind/auxiliary/custom_variant_shorthand.rs
  • crates/biome_css_formatter/src/tailwind/auxiliary/source_inline.rs
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/attribute_selectors.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/boundary_conditions.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/container.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/functions.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/import.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/media_queries.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/options.json
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/pseudo_classes.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/scope.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/supports.css
  • crates/biome_css_formatter/tests/specs/css/delimiter_spacing/url_functions.css
  • crates/biome_formatter/src/lib.rs
  • crates/biome_service/src/file_handlers/css.rs
  • crates/biome_service/src/settings.rs

Comment thread crates/biome_configuration/src/css.rs
Comment thread crates/biome_css_formatter/src/css/auxiliary/bracketed_value.rs Outdated
Comment thread crates/biome_css_formatter/src/tailwind/auxiliary/custom_variant_shorthand.rs Outdated
@ematipico ematipico added this to the Biome v2.5 milestone Apr 19, 2026
@changeset-bot

changeset-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e039333

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

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

@github-actions github-actions Bot removed A-Linter Area: linter A-Parser Area: parser A-Tooling Area: internal tools A-LSP Area: language server protocol labels Apr 24, 2026
@github-actions github-actions Bot removed A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages L-Grit Language: GritQL A-Resolver Area: resolver labels Apr 24, 2026
@codspeed-hq

codspeed-hq Bot commented Apr 24, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 13.74%

❌ 3 regressed benchmarks
✅ 27 untouched benchmarks
⏩ 226 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
css_analyzer[pure_9395922602181450299.css] 23.6 ms 26.9 ms -12.14%
css_analyzer[foundation_11602414662825430680.css] 238.4 ms 276.4 ms -13.74%
css_analyzer[tachyons_11778168428173736564.css] 178.7 ms 198.2 ms -9.82%

Comparing luisherranz:feat/delimiter-spacing-css (e039333) with next (8047bc5)

Open in CodSpeed

Footnotes

  1. 226 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ematipico ematipico merged commit d09400d into biomejs:next Apr 24, 2026
27 of 28 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Formatter Area: formatter A-Project Area: project L-CSS Language: CSS and super languages L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants