Skip to content

project: Fix code_actions_on_format edits being reverted when no formatter available#51605

Merged
SomeoneToIgnore merged 5 commits intozed-industries:mainfrom
mvanhorn:osc/51490-fix-code-actions-format-revert
Mar 23, 2026
Merged

project: Fix code_actions_on_format edits being reverted when no formatter available#51605
SomeoneToIgnore merged 5 commits intozed-industries:mainfrom
mvanhorn:osc/51490-fix-code-actions-format-revert

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

Fixes #51490

Problem

When code_actions_on_format is configured (e.g., source.fixAll.phpcs) and formatter is not explicitly set, code action edits are applied but then reverted during format-on-save.

The root cause is in the formatting pipeline's error handling. Code actions and formatters share a single transaction via .chain(). When Formatter::Auto resolves to Formatter::Prettier (PHP defaults to prettier.allowed: true), and prettier is not installed or fails, the error propagates via ? out of the formatter loop, aborting the entire format_buffer_locally function. This causes the formatting transaction - which already contains the successfully applied code action edits - to be lost.

The workaround of setting "formatter": [] works because it removes all formatters from the chain, so no formatter can fail and trigger the abort.

Fix

Change error handling for individual formatters (Prettier, External command, Language Server) from propagating errors via ? to logging the error and continuing to the next formatter. This treats "formatter failed" as a no-op for that specific formatter rather than aborting the entire pipeline.

This matches the existing pattern used when LanguageServer(Current) finds no server that supports formatting - it already logs and continues rather than erroring.

Testing

  • cargo check -p project passes
  • cargo fmt -p project -- --check passes

Release Notes:

  • Fixed code_actions_on_format edits being reverted when formatter: "auto" resolves to an unavailable formatter.

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Mar 15, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @mvanhorn on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@zed-community-bot zed-community-bot bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Mar 15, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor Author

@cla-bot check

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Mar 15, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @mvanhorn on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Mar 15, 2026

The cla-bot has been summoned, and re-checked this pull request!

@mvanhorn
Copy link
Copy Markdown
Contributor Author

@cla-bot check

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Mar 15, 2026
@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Mar 15, 2026

The cla-bot has been summoned, and re-checked this pull request!

Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

That's a great investigation under the link posted.

It seems that the main idea is that the loop for formatter in formatters must be always carried up to the end, and here we try to fix some of the ? that forbid it.

Yet, things like extend_formatting_transaction are still ?-marked and would need a similar treatment?

Overall, the whole approach seems not very sustainable, we would be better off extracting the entire loop's body into its own function/method.
We could return an anyhow::Result<std::ops::ControlFlow> there and cover all various ? explicitly, once, by logging an error with the context and moving on.


Besides that, the usual ask are the tests: formatting is rather intricate and would definitely benefit from covering this scenario in e.g. editor_tests.rs

@SomeoneToIgnore SomeoneToIgnore self-assigned this Mar 19, 2026
Restructure the formatter loop to extract each formatter's application
logic into a separate function that returns anyhow::Result. On failure,
the loop logs the error and continues to the next formatter instead of
aborting the entire formatting chain.

This addresses the sustainability concern: instead of converting
individual ? operators to match blocks, all error points are covered
by the function boundary.
@mvanhorn mvanhorn force-pushed the osc/51490-fix-code-actions-format-revert branch from 642e7ae to db9886b Compare March 19, 2026 15:15
@github-actions
Copy link
Copy Markdown

📏 PR Size: 902 lines changed (size/XL)

Please note: this PR exceeds the 400 LOC soft limit.

  • Consider splitting into separate PRs if the changes are separable
  • Ensure the PR description includes a guided tour in the "How to Review" section so reviewers know where to start

@mvanhorn
Copy link
Copy Markdown
Contributor Author

Restructured in db9886b - extracted the entire formatter loop body into apply_formatter() which returns anyhow::Result<()>. The caller catches errors, logs them, and continues to the next formatter.

This covers all ? operators at once (including extend_formatting_transaction) rather than converting them individually. The continue statements in the original code become return Ok(()) in the extracted function.

Re: tests - I'll add a test in editor_tests.rs covering the scenario where one formatter fails but subsequent formatters still apply. Working on that next.

Verifies that when one formatter in the chain fails (returns an error),
subsequent formatters still apply their edits successfully. This covers
the scenario described in zed-industries#51490 where code_actions_on_format edits
were lost when a formatter was unavailable.
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Added a test in 106c346 covering the case where one formatter fails and subsequent formatters still apply their edits.

On the Result<ControlFlow> return type - I kept Result<()> because the loop always iterates all formatters regardless of individual outcomes. The two states we need are covered: Err(e) for failures (caller logs and continues) and Ok(()) for success or skip. ControlFlow::Break would mean "stop iterating" which doesn't have a use case here. Happy to change if you prefer though.

Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Thank you!

@SomeoneToIgnore SomeoneToIgnore enabled auto-merge (squash) March 23, 2026 14:16
@SomeoneToIgnore SomeoneToIgnore merged commit d31d3b8 into zed-industries:main Mar 23, 2026
48 of 50 checks passed
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Thanks for the merge! Happy this code_actions_on_format fix landed.

AmaanBilwar pushed a commit to AmaanBilwar/zed that referenced this pull request Mar 23, 2026
…atter available (zed-industries#51605)

Fixes zed-industries#51490

## Problem

When `code_actions_on_format` is configured (e.g.,
`source.fixAll.phpcs`) and `formatter` is not explicitly set, code
action edits are applied but then reverted during format-on-save.

The root cause is in the formatting pipeline's error handling. Code
actions and formatters share a single transaction via `.chain()`. When
`Formatter::Auto` resolves to `Formatter::Prettier` (PHP defaults to
`prettier.allowed: true`), and prettier is not installed or fails, the
error propagates via `?` out of the formatter loop, aborting the entire
`format_buffer_locally` function. This causes the formatting transaction
- which already contains the successfully applied code action edits - to
be lost.

The workaround of setting `"formatter": []` works because it removes all
formatters from the chain, so no formatter can fail and trigger the
abort.

## Fix

Change error handling for individual formatters (Prettier, External
command, Language Server) from propagating errors via `?` to logging the
error and continuing to the next formatter. This treats "formatter
failed" as a no-op for that specific formatter rather than aborting the
entire pipeline.

This matches the existing pattern used when `LanguageServer(Current)`
finds no server that supports formatting - it already logs and
`continue`s rather than erroring.

## Testing

- `cargo check -p project` passes
- `cargo fmt -p project -- --check` passes

Release Notes:

- Fixed `code_actions_on_format` edits being reverted when `formatter:
"auto"` resolves to an unavailable formatter.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions large-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

code_actions_on_format edits are reverted when formatter: "auto" finds no formatter

2 participants